AjaxChat: Chatting, the AJAX Way!

Start building your own AJAX-based chat room

This article presents an AJAX (Asynchronous Java and XML) project done with straight AJAX. That means this application will not be using any library or toolkit—it will just be you and JavaScript! Here we'll build ourselves a nice little multiuser, multiroom chat application, much like the Yahoo! chat rooms. Even better, we'll build this application using Struts on the server to make it that much more interesting!

Requirements and goals

Chat rooms have been with us for a very long time now. In fact, it is chat rooms, those gathering places where you can talk in real time with your fellow computer users, that draw many people to the Internet in the first place (email tends to be the biggest draw, but chat rooms are not too far behind for many people).

If you think about what a chat room application must require, it becomes clear that we'll need a server component. There has to be some broker in between all the chatter that deals with keeping track of the various chat rooms available, who is logged in and chatting in what room, and those types of system-level considerations. More important, though, is some arbiter of messages—a way for all the people currently chatting in a given room to see the various messages.

In a "real" chat application, such as those you might find on AOL, for instance, it is likely that the server actually pushes new messages out to the users in the room. That way, there is no delay between when someone says something and when everyone else in the room sees it.

If you were going to build a purely HTML-based chat application, as we are about to do, you'd have to consider all of these points. However, because the Web is based on a pull model of client-server interaction, ignoring things like applets and such, you clearly need to go about things a little differently. Could you have a meta refresh tag on a page that periodically asked the server for any new messages? Yes, but then you would be redrawing the entire screen each time, which would be rather inefficient, especially if you built it with something like JSP (JavaServer Pages) or another dynamic rendering technology where the server would be responsible for that redrawing.

There must be a better way, and of course there is: AJAX!

The AjaxChat application will have a number of requirements based on these, and a few others, as follows:

  • The application must support multiple users and multiple rooms. We won't enumerate any specific scalability requirement except to say that a reasonably sized group of friends should be able to chat simultaneously, so something in the 10-user range should do fine.
  • We have seen a number of solutions that use servlets on the back end, so this time, we'll do something a little more interesting and robust and build AjaxChat using Struts.
  • We want to be able to adjust the font size of messages displayed in the room we are chatting in, to allow for people (like me!) with bad eyesight to have an easier time of it.
  • Just for fun, we want the ability to show our messages in one color and other people's messages in another.
  • The server will need to properly deal with things like duplicate users and users who do not properly log off. We won't, however, require a login per se—that is, user accounts will not be created and persisted.
  • There should be an option to clear the "history" in a room so that a user can have a clean display at any time.

This should be a fun project! Let's now figure out how we are going to accomplish all that we have set out for ourselves.

Visualizing the finish line

Creating AjaxChat, we first should know what we're building, so what follows is some screenshots and description of AjaxChat. The application essentially consists of three distinct screens: the "login" screen, the list of rooms (called the lobby), and the chat room itself.

Figure 1 shows the login screen. This screen is pretty simple, although we do have some eye candy in the form of a shadowed inset text box and a metallic-looking button. This screen is just a greeting and a place to enter a username. It is not a security login, mind you; it is simply a way for users to give themselves a name to chat under.

Figure 1. The AjaxChat login page. Click on thumbnail to view full-sized image.

The next screen, the room list screen, is shown in Figure 2. This screen shows all the available chat rooms and how many users are chatting in each. It also provides a logout button. This serves to clear out the user's session and ensures that they do not appear to still be chatting in any rooms.

Figure 2. The rooms list page. Click on thumbnail to view full-sized image.

Lastly, we come to the meat and potatoes of AjaxChat: the screen representing a chat room (Figure 3).

Figure 3. Where it all happens: the chat room screen. Click on thumbnail to view full-sized image.

The chat room screen tells us what room we are chatting in and shows a list of the users in the room with it. It provides a place to enter our messages, and a place to see all the messages posted to the room since we entered it. We also have some bells and whistles in the form of two icons for increasing or decreasing the size of the font the message scroll is seen in, as well as select boxes to change our own message's color and the color of all the messages of all the other chatters in the room. Naturally, we have a way to exit the room, and we also have a button that clears the chat history so we can have a nice, clean message display.

This screen is laid out using tables. You may wonder why I did not do this by using CSS (Cascading Style Sheets) layout techniques. The simple answer is that those techniques are still in their infancy, and cross-browser issues remain to be worked out. Tables are still my layout option of choice and will be until those issues are worked out. It is possible to do this screen with CSS and no tables, but it would have been fragile depending on which browser you viewed it in. Better to use the technology that is more ubiquitous and well-behaved at this point in time. Besides, for Web applications, CSS layout is somewhat less interesting simply because the layout tends to not change as much. For Websites, where content delivery is the primary concern, CSS layout has much more to offer.

Enough of the tables versus CSS layout debate—let's get into some code!

Dissecting the solution

Download the full source from the Apress Website and follow along here. There is too much code to commit to print here, but I will call out bits and pieces where appropriate; otherwise, downloading and following along is very important.

To begin, let's look at the file layout of the project, as shown in Figure 4.

Figure 4. Directory structure layout of AjaxChat. Click on thumbnail to view full-sized image.

We see our typical Webapp structure. AjaxChat consists of three JSPs:

  • index.jsp, which is our welcome page and where the user "logs in"
  • lobby.jsp, which is where the chat room list is
  • room.jsp, which, of course, is the JSP for inside a chat room

Interestingly, they go in exactly that order in terms of complexity!

All three of them reference the stylesheet styles.css in the css directory. All three also make use of the buttonBG.gif and textBG.gif images for stylizing buttons. The last two images, zoomDown.gif and zoomUp.gif, are used in the room to provide for text zooming capabilities.

In the inc directory, we find a single file, color_options.inc. This file is included in the room.jsp file when rendered (a server-side include) and contains all the <option>s for the dropdowns where colors can be selected. Because there are so many of them, having it all in one place is definitely advisable.

The WEB-INF directory contains web.xml, as usual for a Java Webapp, and also contains struts-config.xml, the Struts configuration file. In this directory you will also find app-config.xml, which is a custom configuration file where a few parameters for the application are stored. Lastly, you will find rooms-config.xml, which is the file where the chat rooms available to chatters are stored.

The WEB-INF/classes directory is where the beginning of our server-side Java classes are. We can see that there are quite a few, and we'll of course be going over them all in detail. In brief, however, the classes found in the action directory are our Struts Actions. The single class found in the actionform directory is our Struts ActionForm (only one is found here; two others are DynaActionForms defined in struts-config.xml). The single class in the daemon directory is a background thread that will be used to clean up users when they leave the application. The dao directory contains the single data access object AjaxChat uses. The dto directory contains three data transfer object classes used by AjaxChat to store information about rooms, users, and individual messages. In the filter directory, you will find a single servlet filter used to check whether a request references a valid session as far as AjaxChat is concerned. The listener directory contains a single ContextListener that is used to initialize AjaxChat.

Finally, the root ajaxchat directory contains a single class that is used to store some configuration information. And, the WEB-INF/lib folder contains all the libraries that AjaxChat depends on, and they are listed in Table 1.

Table 1. The JARs that AjaxChat depends on, found in WEB-INF.lib

JARDescription
commons-logging-1.0.4.jarJakarta Commons Logging is an abstraction layer that sits on top of a true logging implementation (like log4j), which allows you to switch the underlying logging implementation without affecting your application code. It also provides a simple logger that outputs to System.out, which is what this application uses.
commons-beanutils-1.7.0.jarThe Jakarta Commons BeanUtils library, needed by Digester.
commons-digester-1.7.jarJakarta Commons Digester is a library for parsing XML and generating objects from it. It is used to parse some messages passed to the server by the client code.
commons-lang-2.1.jarJakarta Commons Lang are utility functions that enhance the Java language. Needed by Digester.
commons-validator-1.0.2.jarThe Commons Validator package, used to provide declarative validation in Struts applications.
javawebparts_listener_v1.0_beta3.jarThe Java Web Parts (JWP) listener package, which contains, among other things, a listener for doing application configuration.
javawebparts_core_v1.0_beta3.jarThe JWP core package, required by all other JWP packages.
jstl.jarThe core JAR needed to support JSTL.
standard.jarThe standard set of tags that JSTL provides.
struts-1.2.7.jarThe Struts framework itself.

The client-side code

We'll start our dissection by looking at the configuration files for this application. Let's begin by looking at web.xml.

web.xml

The first context parameter, javax.servlet.jsp.jstl.fmt.localizationContext, tells JSTL (JSP Standard Tag Library) we want to internationalize our messages. It also serves to provide the base filename for the properties file.

Next, we see the configFile context parameter. This will be used by our ContextListener to locate the configuration file for the application. In this case, we are telling it to look for app-config.xml in the WEB-INF directory, relative to the context.

The last context parameter we see is configClass. This is the class that will be populated with our configuration information, com.apress.ajaxprojects.ajaxchat.AjaxChatConfig in this case.

After the context parameters comes a single filter configuration of the SessionChecker filter. We'll go into what that filter does later, but for now it is enough to know that it will fire for every request ending in *.do made to this context.

After the filter configuration comes a ContextListener configuration. Here we are using the AppConfigContextListener that can be found in Java Web Parts. This listener allows us to read in a configuration file and populate a config object in various ways. Here we have a very simple configuration, so the default behavior is used: the named class will be instantiated, each element in the config file will be read in, and the corresponding setter in the instantiated object will be called. This listener can deal with more complex config files, as well as provide a default simple bean for storing the configuration information. Have a look; it is a pretty handy part!

The next section of web.xml declares our Struts Action servlet. Note the detail and debug parameters. This determines the level of logging Struts will do for us.

After that comes configuration of the session timeout. We set the value so that a session will expire after one minute. This is because a session is not established until after the user "logs in," and after that, we have AJAX calls continually firing every few seconds. So a session should never get close to a minute old. If it does, and the session expires, it likely means the user navigated away from the chat application, and we want to catch that situation as quickly as possible so we can clear them out of any room they were in. A minute is usually the smallest value you can set for a session timeout, although some containers may allow for a finer level of granularity. A minute should work for us though.

Lastly, we configure the welcome page to index.jsp, so any request that comes in that does not reference a particular document (or end in *.do) will be routed to this JSP, which is our application's starting point.

struts-config.xml

The next configuration file we'll examine is struts-config.xml. Here we see a mixture of DynaActionForms, as we saw earlier, as well as "regular" ActionForms, that is, one for which you provide a class extending ActionForm. This is done simply to demonstrate that both work fine when doing AJAX in a Struts-based application. The LobbyActionForm and LoginActionForm correspond to the lobby.jsp page and the index.jsp page, respectively. The ajaxPostMessageActionForm is the bean that will be used when posting a message in a room. Many times you will see that an AJAX-based Webapp goes directly to the request object to get incoming parameters, but I wanted to show that an AJAX request that is not XML-based would populate an ActionForm just the same as a non-AJAX request.

After that come the action mappings. They are broken down into those that deal with AJAX requests and those that do not. There is nothing different about them; I just organized them that way for clarity's sake. The mappings are described in Table 2.

Table 2. The Action mappings used in AjaxChat

PathDescription
/loginThis mapping is called when the user clicks the Login button on the starting page (index.jsp).
/logoutThis mapping is called when the user clicks the Logout button on the lobby screen (lobby.jsp).
/lobbyThis mapping is called to show the lobby screen (lobby.jsp). This is essentially a "setup" Action for that screen.
/joinRoomThis mapping is called when the user clicks on one of the chat rooms to enter it on the lobby screen (lobby.jsp).
/leaveRoomThis mapping is called when the user clicks the Leave Room button on the chat room screen (room.jsp).
/ajaxListUsersInRoomThis mapping is called via an AJAX call to refresh the list of users in the room when chatting in a room (the right-hand pane on the side, room.jsp). Note that this is called periodically to keep the list of users fresh.
/ajaxPostMessageThis mapping is called via an AJAX call to post a message to the room the user is currently chatting in (room.jsp).
/ajaxGetMessagesThis mapping is called periodically to get the list of messages posted to the room since the last one the client viewed (room.jsp).
/ajaxLobbyUpdateStatsThis mapping is called periodically to refresh the number of chatters in each room when on the lobby screen (lobby.jsp).

Conceptually, as far as the AJAX-related mappings go, only the ajaxPostMessage mapping is really an "input" mapping, meaning it is the only one that uses an ActionForm. No validation is performed on it, as per the mapping configuration.

The last item in struts-config.xml is the message resource bundle declaration.

app-config.xml

You will find two other configuration files involved in this application: app-config.xml and room-config.xml. The former defines overall application settings, and the latter is a list of the available chat rooms. First is app-config.xml, shown in Listing 1.

Listing 1. app-config.xml for AjaxChat

 <config>
   <!-- maxMessages is the maximum number of messages that will be stored in -->
   <!-- the messages collection of each room. Any time a message is posted -->
   <!-- to a room, the number of messages in the collection is checked -->
   <!-- against this value. If the count exceeds this value, the collection -->
   <!-- is cleared before the new messages is stored. This is just a -->
   <!-- memory-saving measure, and probably makes things a bit more -->
   <!-- efficient too. -->
   <maxMessages>250</maxMessages>
   <!-- userInactivitySeconds is the maximum number of seconds that can -->
   <!-- elapse between Ajax requests before a user is considered inactive. -->
   <!-- When a user is considered inactive, they are forcibly removed -->
   <!-- from all rooms and from the application. This is to deal with the -->
   <!-- case of a user closing the browser window without properly logging -->
   <!-- out, or possibly a JavaScript error that stops the Ajax request -->
   <!-- timers. Note that this was originally done in a SessionListener, -->
   <!-- but because of problems seem in some containers, this had to be -->
   <!-- done instead. -->
   <userInactivitySeconds>15</userInactivitySeconds>
</config> 

The comments pretty much tell the whole story.

rooms-config.xml

Moving on, we come to rooms-config.xml, shown in Listing 2.

Listing 2. rooms-config.xml for AjaxChat

 <rooms>
   <room name="General Topics" />
   <room name="Programming" />
   <room name="Movies" />
   <room name="Music" />
   <room name="Television" />
</rooms> 

This is literally nothing more than a listing of the available chat rooms in XML format. The only information it conveys is the name of the room, so it really is as simple as it looks.

index.jsp

Now that we have the configuration files out of the way, let's move on the JSPs that make up AjaxChat. I am a big believer in simplicity and having as few source files as possible, and so there are a grand total of three JSPs to look at. First up is index.jsp.

Note the use of JSTL to display the page title and the two chunks of text at the top of the page. After that we see this section:

 <logic:messagesPresent>
   <font color="#ff0000">
      <html:messages id="error">
         <bean:write name="error" />
      </html:messages>
   </font>
   <br/><br/>
</logic:messagesPresent> 

Contained within Struts are functions for returning messages to the client. It does this by placing the messages under a known request attribute key. Rather than having to pull that attribute out yourself and see if there were any messages passed back, you can use the <logic:messagesPresent> tag, which will render the content it encloses only if there are messages present. <html:messages> will loop through the messages and display them. Here we tell it to display errors, which is a known value to Struts. The <bean:write> tag literally writes out the error message.

This page contains a single form where the user can enter their username. One neat trick is this part:

 <html:submit styleClass="cssButton">
   <fmt:message key="labels.loginButton" />
</html:submit> 

You cannot embed one custom tag within another, which means this won't work:

 <html:submit styleClass="cssButton"value="<fmt:message key="labels.loginButton" />" /> 

This will not be interpreted and will result in an exception being thrown. The solution is to use the fact that a <submit> tag can wrap some text, which becomes the value, that is, the label, of the button. In this way, we can still use our internationalized message bundle.

lobby.jsp

Next is the JSP lobby.jsp. Let's talk about the markup first, since it is simpler. First, we display a heading at the top and the version of the application. We then show a message for the user. After this is the <div> with the id roomList:

 <div id="roomList">
   <c:forEach var="roomName" items="${LobbyActionForm.rooms}">
      <a href="<c:url value="joinRoom.do">
         <c:param name="name" value="${roomName}" /></c:url>">
         <c:out value="${roomName}" />
      </a>
      <br/>
   </c:forEach>
</div>
<br/> 

Here we are introduced to some new JSTL tags. First is the <c:forEach> tag. This tag accepts an EL (expression language) expression in the items attribute that tells it to find a bean named LobbyActionForm in any scope (request scope in this case) and iterate over the rooms collection in it, which, in this case, is a simple Java String, and, for each element, present it to us under the name roomName. If we instead had a collection of RoomDTO objects in the rooms collection, I might have set the var attribute to room, and then later I could reference individual properties of the object by modifying the EL expressions. Here, though, we have just a String. We are constructing a URL from this that the user can click to join the room, so we use the <c:url>, which constructs a URL for us. We need to append a parameter named name to the URL as a query string and give it the value of the room name. Because the room names could have spaces or other problematic characters in a URL, using the <c:param> tag inside the <c:url> tag is good because it deals with encoding the parameter properly for us. After that, we use the <c:out> tag, which writes out the value named. In this case, we use an EL expression, which tells the tag to write out the roomName variable, which is our room name. Just to drive the point home (especially if JSTL is new to you), if we were dealing with a collection of RoomDTO objects instead of just Strings, and var had the value room instead, the EL expressions could be ${room.roomName} instead, to reference the roomName property of the current RoomDTO object referenced by the room variable.

The interesting thing to note is that this whole section is only important when the page is first shown. After that, we'll have a periodic AJAX event that overwrites the contents of the <div>. While it is true that what is written out is essentially the same, with the addition of the number of chatters in each room, the difference is that it will be written from with a Struts Action and will not use JSTL like this.

With the markup complete, we can now shift our attention to the JavaScript on the page. First, we encounter a few page-scoped variables, as summarized in Table 3.

Table 3. Page-scoped variables on lobby.jsp

VariableDescription
assureUnique Internet Explorer is perhaps a little overaggressive in its caching scheme with regard to GET requests. If the URL is identical, IE will use a previously cached result, if any. Here this would mean that the stats would never be updated after the first time. To avoid this, we append a parameter to the requested URL that makes it unique with every request, thereby avoiding this problem.
xhrLobbyUpdateStats This is a reference to the XMLHttpRequest object that will be used to fire the periodic AJAX stats update event.
timerLobbyUpdateStats This is a reference to the timer that will fire to make our periodic stats update happen.
lobbyUpdateStatsDelay This is the number of milliseconds between stats update requests. The default is 2 seconds (2,000 milliseconds).
sendAjaxRequest When the user clicks the logout button, it is possible for an AJAX event to fire after the session has been invalidated, which leads to an exception being thrown. So, when the logout button is clicked, this variable gets set to false, which will cause no subsequent AJAX events to fire.

On page load, the init() function is called. Its sole purpose is to start up the timer that will fire our periodic AJAX stats update request. The function that the timer points to is lobbyUpdateStats():

 /**
* This function is called as a result of the firing of the
* timerLobbyUpdateStats timer to make an AJAX request to get counts of
* users chatting in each room. This happens continually as the user
* sits in the lobby.
*/
function lobbyUpdateStats() {
   // Only fire a new event if a previous one has completed, or if the
   // XMLHttpRequest object is in an uninitialized state, or one has not
   // yet been instantiated.
   if (xhrLobbyUpdateStats == null || xhrLobbyUpdateStats.readyState == 0 ||
      xhrLobbyUpdateStats.readyState == 4) {
      // Create XMLHttpRequest object instance based on browser type.
      try {
         if (window.XMLHttpRequest){
            xhrLobbyUpdateStats = new XMLHttpRequest();
         } else {
            xhrLobbyUpdateStats = new ActiveXObject('Microsoft.XMLHTTP');
         }
         // Set the JavaScript function that will act as a callback for
         // any events the instance fires.
         xhrLobbyUpdateStats.onreadystatechange = lobbyUpdateStatsHandler;
         // Set the target URI for the request. Note that we append a
         // value that will ensure that the URL is always unique.
         // This is to deal with caching issues in IE.
         target = "<html:rewrite action="ajaxLobbyUpdateStats" />" +
            "?assureUnique=" + assureUnique++;
         // One minor problem that
         if (sendAJAXRequest) {
            xhrLobbyUpdateStats.open("get", target, true);
            xhrLobbyUpdateStats.send(null);
         }
      } catch(e) {
         alert("Error in lobbyUpdateStats() - " + e.message);
      }
}
// Restart the timer no matter what happened above.
timerLobbyUpdateStats = setTimeout("lobbyUpdateStats()",
   lobbyUpdateStatsDelay);
} // End lobbyUpdateStats(). 

First we do a check of the status of the previous AJAX request, if any. If one had fired, xhrLobbyUpdateStats would not be null. In that case, we also want to make sure the request completed or no request took place. Only then should a new event fire.

Once that determination is made, we instantiate a new XMLHttpRequest object. We point it to the lobbyUpdateStatsHandler callback function and set the URL appropriately. Notice the use of the <html:rewrite> tag. This is a Struts tag from the HTML taglib that writes a proper URL based on a given action mapping. It takes into account the context, current location of the page, and so forth to render a URL that will work properly. Also, we append the assureUnique query string parameter to get around the previously mentioned IE caching concern. After that, we fire the request, but only if the sendAjaxRequest variable is true. Note that this is done as late in the process as possible so that the user clicking the logout button would register as close to the request being sent as possible in order to avoid micro-timing issues where the two events are close together, but not quite close enough, and the AJAX request fires after the button has been clicked. If that were to happen, it is possible for the session to have been invalidated before the AJAX request hits the server, in which case, an exception occurs. While that scenario is technically still possible, checking the variable at this point rather than earlier in the function reduces the chances of it happening as much as possible. Lastly, we fire the timer again so that the next AJAX event will fire. Note that this happens regardless of whether the request gets sent, thereby setting up a continuous loop of requests (or at least potential requests).

Once the AJAX request returns, control winds up in the lobbyUpdateStatsHandler() function:

 /**
* This is the Ajax callback handler that updates the display.
*/
function lobbyUpdateStatsHandler() {
if (xhrLobbyUpdateStats.readyState == 4) {
   if (xhrLobbyUpdateStats.status == 200) {
      // Get the returned XML and parse it, creating our HTML for display.
      newHTML = "";
      msgDOM = xhrLobbyUpdateStats.responseXML;
      root = msgDOM.getElementsByTagName("rooms")[0];
      rooms = root.getElementsByTagName("room");
      for (i = 0; i < rooms.length; i++) {
         room = rooms[i];
         roomName = room.getAttribute("name");
         users = room.getAttribute("users");
         url = "<a href=\"<c:url value="joinRoom.do?name=" />";
         newHTML += url + escape(roomName) + "\">" + roomName + "</a>";
         newHTML += " (" + users + ")<br/>";
      }
      // Update the display.
      objRoomList = document.getElementById("roomList");
      objRoomList.innerHTML = newHTML;
   } else {
      alert("Error in lobbyUpdateStatsHandler() - " +
         xhrLobbyUpdateStats.status);
   }
}
// End lobbyUpdateStatsHandler(). 

First, the result of the event is checked to make sure it completed successfully. Remember that this function will be called numerous times during the lifecycle of the request, but we really only care about when it completes successfully. Our response is a chunk of XML in the form

 <rooms>
<room name="xxx" users="yyy" />
</rooms> 

This is relatively easy to parse. In fact, all we do is get a reference to the XML DOM (Document Object Model) through the responseXML property of XMLHttpRequest. We then get the root node (rooms), and then, from that, we get the collection of <room> elements. We then iterate over that collection, and for each we get the name and users attributes. From that, we construct the appropriate markup.

Note the use of the <c:url> tag here, which we use so that if cookies are disabled, the URL will be rewritten to include the session ID. You should realize that this executes when the page is rendered, not on the client when the function is called.

In contrast to the <c:url> tag, note that here we have to handle escaping the room name ourselves since we cannot execute custom tags on the client, and it is at that point that we would know the room name, not when the page is rendered. So, while this produces similar markup to what we saw earlier with JSTL and Struts tags (the only difference is the addition of the user counts here), we cannot use those tags now because their chance to execute has already passed. Instead, we construct it using simple string manipulations and using the JavaScript escape() function to URL-encode the room name appropriately. escape() is a built-in function of JavaScript used to URL-encode a string. There is a corresponding unescape() function as well to reverse the encoding process.

Once the string of markup is completely formed, we update the roomList <div>, and our chore is done! Note too that we'll get an error pop-up if anything but an OK response is received from the server. This is not especially robust error handling, but it gets the job done. Since chances are that a non-200 HTTP result is unrecoverable anyway, there is no point in trying to handle this anymore gracefully than we have.

room.jsp

Finally, we come to the page for the chat room itself, room.jsp. Let's again examine the markup first. I do not want to go over all of it because it is, by and large, just plain old HTML with some JSTL mixed in to insert content and you will likely have no trouble examining it yourself. Overall, though, we are looking at a table structure that contains four rows. The first row is the header that stretches across the entire window and displays the current room. The next row contains the chat history area and the user list area. Note that the user list spans across three rows to take up the remainder of the window after the header row. The third row is where the user controls are found, and the fourth row is where the user text input box is found.

One note of interest is in the header. You see the following line:

 <c:out value="${roomName}" />

This is displaying the value of an object named roomName, which happens to be in session scope, and is set when the user enters a room. It is simply a String. Here we are using an EL expression to display it.

You may momentarily get a quizzical look on your face when you see this piece of code:

 <input type="button"
   value="<fmt:message key="labels.leaveRoomButton" />"
   class="cssButton" onClick="leaveRoom();" /> 

You may be saying to yourself, "Wait a minute—a few pages back you said you could not nest one tag inside another, yet here you are doing exactly that!" The difference is that you cannot nest custom tags within each other. Here we are nesting a JSTL tag within a plain old HTML tag. That works just fine. Earlier, I mentioned you could not set the value of the value attribute on a Struts <html:submit> tag with a JSTL tag, and that is correct. Those are two custom tags, two entities that the JSP interpreter has to interpret. Here it is just inserting a dynamic value into a static piece of text that just happens to be an HTML tag. Incidentally, I had that same quizzical look on my own face for a few seconds over this distinction too!

You will notice the mouse event handlers on the two magnifying glass icons. These set the cursor to a pointer hand (or an arrow on some operating systems and browsers) when you hover over them. This serves as a nice visual cue that it is an interactive element. It is always a good idea to provide such cues to your user whenever possible. This will lead to less confusion over what they can do at any given time, and also gives a more polished appearance to your Webapps.

Lastly, notice that the values for the color dropdowns are included. Because both <select> boxes have the same values, it would make no sense to duplicate that in both places; hence, an include avoids the redundant code.

Now it is time to look at the JavaScript that makes this all work. As I'm sure you can guess, this is where most of the action takes place. First up are again some page-scoped variables, as shown in Table 4.

Table 4. Page-scoped variables on room.jsp

Variable Description
AssureUniqueUsersInRoom This is analogous to the assureUnique variable we saw in the lobby.jsp page. This is used to ensure a unique request when updating the list of chatters in the room.
assureUniqueGetMessages This is used to ensure a unique request when retrieving messages.
scrollChatFontSize This is the font size the chat history pane is currently displayed in.
oldUserListHTML When we retrieve an updated list of users in the room, we compare it to the HTML currently being displayed. If it is different than this value, and only if it is different, we update the display. This helps avoid a nasty flickering issue that plagues some versions of Firefox. The flicker, while actually still occurring, is barely perceptible because it happens only when people join and leave the room instead of with each request, and this happens infrequently, relative to the frequency of the regular requests at least.
xhrListUsersInRoom The XMLHttpRequest object used to retrieve the updated list of users chatting in the room.
XhrGetMessagesThe XMLHttpRequest object used to retrieve the messages posted to the room since the last update request was sent.
timerListUsersInRoom This is the timer that fires periodically to update the user list.
timerGetMessages This is the timer that fires periodically to update the chat history pane.
listUsersInRoomDelay This is the delay between updates of the user list (defaults to 2 seconds).
getMessagesDelay This is the delay between updates of the chat history pane (defaults to 1 second).

The room screen works by having two timers running at all times: one to update the list of messages posted to the room, and the other to update the list of users chatting in the room. Both of these timers are initially kicked off in the init() function, called in response to the onLoad event. That is all the init() function does.

The listUsersInRoom() is the function that the timer fires for updating the user list. If you compare this to the lobbyUpdateStats() function on the lobby.jsp page, you will see that they are virtually identical. For this reason, I'll refrain from going over it in detail. In fact, the same is true of the getMessages() function, which is the function fired in response to the other timer. Therefore, I won't go over it either. Please do look at it, though, to convince yourself that it is not doing anything differently than you already saw on the lobby.jsp page.

The listUsersInRoomHandler() function is the callback that does the work when the AJAX call to update the user list returns. It looks like this:

 /**
* This function handles state changes for the listUsersInRoom Ajax request.
*/
function listUsersInRoomHandler() {
   if (xhrListUsersInRoom.readyState == 4) {
      if (xhrListUsersInRoom.status == 200) {
      // Get the returned XML and parse it, creating our HTML for display.
      newHTML = "";
      msgDOM = xhrListUsersInRoom.responseXML;
      root = msgDOM.getElementsByTagName("users")[0];
      users = root.getElementsByTagName("user");
      for (i = 0; i < users.length; i++) {
         newHTML += users[i].getAttribute("name") + "<br/>";
      }
      // Update the display.
      if (oldUserListHTML != newHTML) {
         oldUserListHTML = newHTML;
         document.getElementById("userList").innerHTML = newHTML;
      }
   } else {
      alert("Error in listUsersInRoomHandler() - " + xhrListUsersInRoom.status);
   }
}
// End listUsersInRoomHandler(). 

We are again receiving a response in the form of XML, which looks like this:

 <users>
<user name="xxx" />
</users> 

To parse this document, we start by getting the root element. As we saw previously, we do this by getting a collection of all <users> elements and then accessing the first element of the array. From there, we get a collection of all the <user> elements. We then iterate over that collection and construct some HTML for each, which is, in this case, just the username with a line break after it. We now compare this constructed HTML to the HTML that was last inserted into the userList <div>. We do this by comparing what we just built to the value of the oldUserListHTML variable, which we stored the last time we updated the <div>. You may be wondering why this variable is needed rather than just retrieving the innerHTML property of the <div>. This is because what some browsers return for the value of that attribute does not precisely match what you inserted. The browser may make minor changes (which do not affect what actually is displayed), such as inserting or removing line breaks. If the browser does this, then our comparison would always yield a false result, and we would therefore be updating the <div> more often than we need to. If the value of oldUserListHTML is identical to what we just built, there is no reason to update the <div>. In Firefox at least, there is a flicker every time you update the <div> (this is a known issue, at least in some versions of Firefox). So, we do not want to update the <div> anymore than we need to so that the flickering is kept to a minimum. Once our HTML is formed and we have confirmed it is different than what is there now, we insert it into the userList <div> by setting its innerHTML property. And that is how we update a user list!

The next function we come across is the getMessagesHandler(), the callback for the AJAX call that updates the list of messages posted to the room. This is far and away the most complex bit of JavaScript in this application, but even still, it follows a very typical flow: get an XML document, get a collection of some set of tags, iterate over the collection constructing some HTML markup from it, and then insert that markup into a <div>. The XML we are getting back from the server this time looks like this:

 <messages>
   <message>
      <postedBy>xxx</postedBy>
      <postedDateTime>yyyy</postedDateTime>
      <msgText>zzzz</msgText>
      </message>
</messages> 

So we begin with the usual: get the root element <messages>. Then, get a collection of all <message> elements and begin to iterate over it.

As we construct the HTML for each, we find ourselves using the getElementsByTagName() function, which is new. The syntax we wind up using, to get the postedBy value for instance, is

 postedBy = message.getElementsByTagName("postedBy")[0].firstChild.nodeValue;

The message is the reference to the <message> element we are currently processing. We request a collection of all the <postedBy> elements under this <message> element. This returns an array again, and since we know we have to deal with only one of them, we again go after the first element in the array. From there, we ask for the first child node of that <postedBy> element by accessing the firstChild property. Finally, we grab the nodeValue property, which is what we ultimately were after. It may seem a little convoluted, but that's XML parsing in JavaScript without a library!

This is the way each of the elements under <message> is dealt with. However, recall that we can have separate colors for our messages and for others. The code that accomplishes that is

 txtColor = "";
if (postedBy == "<c:out value="${user.username}" />") {
   txtColor = document.getElementById("yourColor").value;
} else {
   txtColor = document.getElementById("theirColor").value;
}
newHTML += "<font color=\"" + txtColor + "\">" +
   "[" + postedDateTime + "] " + postedBy + ": " + msgText +
   "</font><br/>"; 

You can see another JSTL tag used here that is doing nothing but outputting the username field of the user object found in session scope. Once again, as we saw with the <c:url> tag previously, this is evaluated when this page is rendered, which means this comparison is actually comparing postedBy to a static string. We match this against the username of the person who posted the message we are working on and set the color appropriately based on the selections in the dropdowns.

There's one last piece of code to look at in this function:

 // Update the display. Note that the first time through we want to
// completely overwrite what's there (just &nbsp;), all other times
// we want to add on to what's there. This is done to avoid the
// borders collapsing and there being a minor visual glitch.
objChatScroll = document.getElementById("chatScroll");
if (newHTML != "") {
   if (objChatScroll.innerHTML == "&nbsp;") {
      objChatScroll.innerHTML = newHTML;
   } else {
      objChatScroll.innerHTML += newHTML;
   }
}
// Lastly, always scroll to the bottom.
objChatScroll.scrollTop = 1000000; 

When the page is first displayed, the content of our chat history pane is just a nonbreaking space entity (&nbsp;), which needs to be overwritten the first time we update that pane. Any time after that, though, we need to append our markup to it so that we achieve an actual history display. So we check the current value of the history pane and, if it's &nbsp, then we overwrite; otherwise we append. Lastly, we update the scrollTop property of the history pane so that no matter what happens, we'll be at the bottom of the history so that we'll see the most recent messages immediately.

After this comes the leaveRoom() method, which is a one-liner that is called when you click the Leave Room button. It simply sets the location property of the window to bring the user back to the lobby screen. Notice the use of the Struts <html:rewrite> tag to get a URL appropriate for the task, using the leaveRoom action mapping. Nice to not have to write the URL yourself, is it not? More important, if you rearrange your pages and change your action mappings, Struts will take care of it for you; there's no need to update the JSP (so long as the mapping name doesn't change).

The clearHistory() function comes next, which is called in response to clicking the Clear History button, obviously enough! It simply sets the innerHTML of the chat history pane to the &nbsp; entity, which mimics its starting state.

After that is a utility function, fullTrim(). It accepts a string, trims all whitespace from both ends of it, and returns the result. This function is used when posting a message to make sure the user actually entered something, which we'll see shortly. In fact, we'll see that right now!

The postMessage() function is called when the user clicks the Send button. It spawns an instance of XMLHttpRequest and sends the text the user entered as a query string parameter. It, of course, uses the escape() function to properly URL-encode the string; otherwise, things would break pretty quickly during a conversation! Once the message is sent, we also clear out the user entry text box and set focus to it, which is how most chat clients work, so users will probably expect it here as well. Notice that the new message is not manually put into the history on the client. Instead, it will only be shown in the history when the next update cycle completes. This saves us from dealing with some synchronization complexities and also ensures that the message actually got posted to the room.

Following postMessage() are two final functions: increaseChatScrollFontSize() and its bizarre evil twin, decreaseChatScrollFontSize(). Both of these functions work by changing the value of the scrollChatFontSize variable by 2 in the appropriate direction. It then checks the value to be sure it is 8 or more and 48 or less, which are the lower and upper bounds allowed. Finally, it sets the fontSize property of the style object of the chattScroll <div> to the new value. Note that it appends the string "pt" to the end. Firefox requires this, while IE does not, although it works just fine in IE, so this is a nice cross-browser way to change the font size.

AjaxChat uses a stylesheet named styles.css in the /css subdirectory. It is a fairly straightforward stylesheet, so we won't examine it in detail here. One thing I do want to point out, however, is the use of the overflow attribute on the cssRoomUserList and cssRoomChatScroll classes. Setting these attributes to scroll causes the <div> to have scroll bars when the content is larger than its area. That is how we get vertical scroll bars on those sections. They may appear to be inline frames, but they aren't—they are simply layers.

Lastly, as far as presentation goes, we find a file named color_options.inc in the /inc subdirectory. This contains the markup for all the options contained in our color selection dropdowns, a small sample of which is seen here:

 <option value="#f0f8ff" style="color:#f0f8ff;">Alice Blue</option>
<option value="#faebd7" style="color:#faebd7;">Antique White</option>
<option value="#00ffff" style="color:#00ffff;">Aqua</option>
<option value="#7fffd4" style="color:#7fffd4;">Aquamarine</option>
<option value="#f0ffff" style="color:#f0ffff;">Azure</option> 

Summary

In this article, we examined the AjaxChat application, an AJAX-ified version of the venerable Internet chat application. We saw how, instead of resorting to a library, we can use straight AJAX code without it becoming a nightmare of tangled spaghetti. Part 2 of this book excerpt will present the server-side code.

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.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more