MSc-IT Study Material
June 2010 Edition

Computer Science Department, University of Cape Town

Cookies and session tracking

This example shows you how to track users visiting your site.

HTTP has a specific problem: when a Web server receives a request for a particular page, let us say page A, followed by another request for a page, let us say page B, the server has no easy way to tell if the same user has made both requests, or if the two requests have come from two different users. This complicates the creation of a Web application: the Web application must, in some way, be able to track its users as they move from page to page, and the application must implement a method to do this itself. There are a number of ways to do this, the most simple of which is to use HTTP cookies.

A cookie is text which the Web application sends to the Web browser. The browser stores this text and sends it to the server on every HTTP request which it makes. A cookie can generally store up to four kilobytes of any text a Web application chooses to store in it. This text can be, for example, a unique ID to identify the user. Because the cookie is returned to the server with every HTTP request, the unique ID can be used to identify which user is accessing the page, and so can be used to track the user as they move across the different pages in a Web application. This is important for, say, for letting any changes the user made to the Web application persist while the user is using the site.

The following example shows how to implement a simple hit counter. A hit counter tracks the number of unique visits that a Web page receives. Our Web counter will not count multiple visits from the same person on the same day, so that no one person can arbitrarily increase the number of hits a site seems to be receiving.

The counter we will create will use a file called “counts.txt” which will store the number of hits the page has received. To begin, create a new directory in the webapps subdirectory called “counter”. We will only have one page in the Web application, which will be implemented with the following Servlet:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

class CounterException extends ServletException {
    public CounterException(String message)
    {
	super(message);
    }
}
			       
public class Counter extends HttpServlet implements SingleThreadModel {

    private final String file_name = "counts.txt";
    private final String cookie_name = "visited";
    private final String cookie_value = "returnSoon";

    private String getCookieValue(HttpServletRequest request,
				  String name) throws CounterException
    {
	Cookie cookies[] = request.getCookies();
	Cookie cookie;

	if (cookies == null)
	    throw new CounterException("No cookies have been set");
	
	for (int i = 0; i < cookies.length; i++) {
	    cookie = cookies[i];
	    if (cookie.getName().equals(name))
		return cookie.getValue();
	}

	throw new CounterException("Unable to find a cookie named " +
				   name);
    }

    private final String deleteCookieJavaScript(String name)
    {
	return "var date = new Date();\n" +
	    "document.cookie = '" + name + "=deleted;" +
	    "expires=' + date.toGMTString() + ';;';\n" +
	    "alert('cookie deleted.');";
    }

    
    public void doGet(HttpServletRequest request,
		      HttpServletResponse response)
    {
	BufferedReader file = null;
	int count = 0;

	try {
	    file = new BufferedReader(new FileReader(file_name));
	    count = Integer.parseInt(file.readLine());
	}
	catch(IOException e) {
	}
	finally {
	    try {
		if (file != null)
		    file.close();
	    }
	    catch (IOException b) {}
	}


	String value = null;
	try {
	    value = getCookieValue(request, cookie_name);
	}
	catch(CounterException e) {
	    Cookie cookie = new Cookie(cookie_name, cookie_value);
	    cookie.setMaxAge(60 * 60 * 24);
	    response.addCookie(cookie);
	    count += 1;		
	}
	

	PrintWriter writer = null;
	try  {
	    writer = new PrintWriter(new FileWriter(file_name));
	    writer.println(count);
	    writer.close();
	    writer = null;
	    
	    
	    PrintWriter out = response.getWriter();
	    out.println("<html>");
	    out.println("\t<head><title>Hello, visitor number " + count +
			".</title></head>");
	    out.println("<body>");
	    out.println("<p>Hello, visitor number " + count + ".</p>");
	    out.println("<form><input type='button' value='delete cookie' onclick=\"" + deleteCookieJavaScript(cookie_name) + "\"><input type='submit' value='reload'></form>");
	    out.println("</body>");
	    out.println("</html>");
	}
	catch (IOException e) {
	}
	finally {
	    if (writer != null)
		writer.close();
	}

    }
}
    

The web.xml file is as follows:

<?xml version="1.0"?>

<!DOCTYPE Web-app 
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
    "http://java.sun.com/dtd/Web-app_2_3.dtd">

<Web-app>
  <display-name>The counter application</display-name>
  <description>
    This application counts the number of unique visits it receives.
  </description>

  <servlet>
    <servlet-name>Counter</servlet-name>
    <description>
      A simple Servlet that uses cookies to track the number of unique
      visits it receives per day.
    </description>

    <servlet-class>Counter</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>Counter</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</Web-app>
    

As usual, make sure that the .class file and web.xml are placed in their appropriate directories.

Let us look at the doGet() method. It begins like so:

	BufferedReader file = null;
	int count = 0;

	try {
	    file = new BufferedReader(new FileReader(file_name));
	    count = Integer.parseInt(file.readLine());
	}
	catch(IOException e) {
	}
	finally {
	    try {
		if (file != null)
		    file.close();
	    }
	catch (IOException b) {}
	}
    

This code attempts to open the “counts.txt” file and reads in the single integer value that it contains. The integer “count” is initialized to the value zero. The code in the try block performs the actual file read. It needs to handle two exceptional situations: a.) the first time that the application is run, the file will not yet exist; b.) the Servlet may have a problem reading the file in general. Both problems will throw exceptions of type IOException, which we catch. We use a finally block to ensure that the file is always closed if it has been created. If an exception has occurred, we do nothing except let the doGet() method continue. This is perfectly fine: nearly all of the exceptions will have occurred because the “counts.txt” file does not exist. In this case, the count variable should be set to zero, which we have ensured it is when we declared it. More fine grained error control could be obtained by reporting an error for IOException (how to do this is explained later), and also catching the FileNotFoundException, in which case we do nothing and let the method continue, as above.

Now that we have read the hit count (if it so far exists), we need to decide if the counter should be incremented. This Web application uses cookies to determine if a user has previously visited the page. The following code examines if a cookie had previously been stored on the user's computer:

	String value = null;
	try {
	    value = getCookieValue(request, cookie_name);
	}
	catch(CounterException e) {
	    Cookie cookie = new Cookie(cookie_name, cookie_value);
	    cookie.setMaxAge(60 * 60 * 24);
	    response.addCookie(cookie);
	    count += 1;		
	}
    

getCookieValue() will throw an exception if the “visited” cookie has not been set. If this happens, the Web application creates a new cookie (which is, conveniently, an object of type Cookie) and tells the response object to store it on the users computer. The counter is also incremented. Notice that the counter is not incremented if the cookie exists. The Cookie constructor takes two arguments: the first is the cookie's name, and the second is an arbitrary text value which must be smaller than four kilobytes (a fairly substantial amount of text). By default, a cookie will only last for as long as the browser is open – the cookie is deleted when the user shuts their browser down. However, we would like the cookie to be stored on the user's computer for a whole day, so that our hit counter does not count multiple visits made on the same day. This is done using the Cookie object's setMaxAge() method. It accepts the number of seconds for which the cookie must be stored on the computer. We supply it with the number of seconds in a day (60 seconds in a minute, 60 minutes in an hour, 24 hours in a day).

The work of reading the cookie is done by the getCookieValue() method :

    private String getCookieValue(HttpServletRequest request,
				  String name) throws CounterException
    {
	Cookie cookies[] = request.getCookies();
	Cookie cookie;

	if (cookies == null)
	    throw new CounterException("No cookies have been set");
	
	for (int i = 0; i < cookies.length; i++) {
	    cookie = cookies[i];
	    if (cookie.getName().equals(name))
		return cookie.getValue();
	}

	throw new CounterException("Unable to find a cookie named " +
				   name);
    }
    

A Web application can store multiple cookies (each with a different name) on a computer. You can gain access to the cookies using the request object's getCookies() method, which will return an array of cookies. getCookieValue() searches the array for the Cookie with the given name, and returns its value. The first time the application is run, getCookies() will return null, since no cookies have yet been set. In this case, or in the case where the requested cookie isn't found, we throw an exception of type CounterException.

Finally, we return some HTML and update the counts.txt file.

	PrintWriter writer = null;
	try  {
	    writer = new PrintWriter(new FileWriter(file_name));
	    writer.println(count);
	    
	    PrintWriter out = response.getWriter();
	    out.println("<html>");
	    out.println("\t<head><title>Hello, visitor number " + count +
			".</title></head>");
	    out.println("<body>");
	    out.println("<p>Hello, visitor number " + count + ".</p>");
	    out.println("<form><input type='button' value='delete cookie' onclick=\"" + deleteCookieJavaScript(cookie_name) + "\"><input type='submit' value='reload'></form>");
	    out.println("</body>");
	    out.println("</html>");
	}
	catch (IOException e) {
	}
	finally {
	    if (writer != null)
		writer.close();
	}
    

The first few lines opens “counts.txt” for reading, and writes the new counts value. Notice that this application can be streamlined by only writing to the file if the count variable had been incremented.

Next, we output the actual HTML. The page prints a greeting to the visitor, and tells them what the hit count is. Also, to test the application, it provides two buttons. One of which is a submit button which you can use to reload the page. The other shows how to delete cookies by setting their maximum age to zero. This can either be done in the Web application using setMaxAge(0), or it can be done as we do it here, using JavaScript. This code also catches IOExceptions, which can be thrown when writing to either the counts.txt file or the response object. The writer object is also closed in a finally block, to ensure that it is always closed.

The Web application should, when you load it (http://localhost:8080/count/), tell you the number of times the site has been visited. Pressing the reload button does not increment this number, except after you have pressed the 'delete cookie' button. You can completely reset the counter by removing the “counts.txt” file, which should be in Tomcat's bin directory.

Our above Servlet implements the SingleThreadModel interface. It does this because, typically, a Web application is used by multiple users at the same time, each of them trying to view this same page. Tomcat spawns a thread to handle each user, and each thread will run the doGet() method. This makes it possible that each thread may be trying to read and write from “counts.txt” at the same time. Clearly this could cause a problem (consider what would happen if one file reads counts.txt, updates the count variable, but before it can write the new value to the file another thread updates the file instead; now this update is going to be overwritten with an old value). The simplest solution, and the one used here, is to declare the class as implementing SingleThreadModel. This is an interface with no methods – all it does is to tell Tomcat to never use more than one thread at a time for this Servlet. This ensures that there is never a problem with multiple threads attempting to read and write to the file. This does have some performance implications for your Web application: it means that each user viewing the page must wait in turn for Tomcat to finish serving the page to the previous users. This is not an ideal solution when the number of users visiting the site increases. Because of this, and because Java already has its own techniques for handling synchronisation, SingleThreadModel has been deprecated, and no replacement has been offered. You should be using the concurrency mechanisms offered by the Java language to ensure that this type of problem does not occur – unfortunately a discussion of these methods is beyond the scope of these notes, but we do leave you with a simple example to show how this could be done:

    public void doGet(HttpServletRequest request,
		      HttpServletResponse response)
    {
	BufferedReader file = null;
	int count = 0;

	synchronized (this) {
	    try {
		file = new BufferedReader(new FileReader(file_name));
		count = Integer.parseInt(file.readLine());
	    }
	    catch(IOException e) {
	    }
	    finally {
		try {
		    if (file != null)
			file.close();
		}
		catch (IOException b) {}
	    }


	    String value = null;
	    try {
		value = getCookieValue(request, cookie_name);
	    }
	    catch(CounterException e) {
		Cookie cookie = new Cookie(cookie_name, cookie_value);
		cookie.setMaxAge(60 * 60 * 24);
		response.addCookie(cookie);
		count += 1;		
	    }
	

	    PrintWriter writer = null;
	    try  {
		writer = new PrintWriter(new FileWriter(file_name));
		writer.println(count);
		writer.close();
		writer = null;
	    }
	    catch (IOException e) {
	    }
	    finally {
		if (writer != null)
		    writer.close();
	    }
	}
	    
	try {   
	    PrintWriter out = response.getWriter();
	    out.println("<html>");
	    out.println("\t<head><title>Hello, visitor number " + count +
			".</title></head>");
	    out.println("<body>");
	    out.println("<p>Hello, visitor number " + count + ".</p>");
	    out.println("<form><input type='button' value='delete cookie' onclick=\"" + deleteCookieJavaScript(cookie_name) + "\"><input type='submit' value='reload'></form>");
	    out.println("</body>");
	    out.println("</html>");
	}
	catch (IOException e) {
	}

    }
    

Notice that all of the instructions concerning the counts.txt file has been placed into a synchronized block.