Sessions, Cookies, and URLConnections

Mark Wutka shows you how to create a custom URLConnection class to support cookies in a standalone application.

Mark Wutka shows you how to create a custom URLConnection class to
support cookies in a standalone application.

Mark is the author of Special Edition Using Java Server Pages and Servlets
(2000, Que).

If you've used the World Wide Web for any length of time, you've probably heard
about cookies. A cookie is simply a piece of data that the Web server stores
in your browser. A browser and a Web server spend most of their time disconnected
from each other. The only time they are aware of each other's presence is when
the browser retrieves pages, images, or other content from the server. To provide
customized content, the Web server needs a way to identify the browser. The
cookie solves this problem. The Web browser sends a cookie to the browser. When
the browser accesses the server again, it sends the cookie back to the server.
That way, the server can distinguish one user from another.

For example, when you log in to a custom news site, the Web server stores a
cookie on your browser identifying you. Whenever the browser accesses the Web
server, it sends the cookie to the Web server so that the server can give you
the news you want.

Java Server Pages and servlets let you store cookies in the browser and retrieve
them. Even if you don't use cookies explicitly, you may be using them unknowingly.
The HttpSession class, which saves data for a particular client session,
uses cookies to identify browser sessions.

The session mechanism works well when you use a browser (assuming that the
browser supports cookies), but it doesn't work so well from outside a browser.
The problem is that Java's URLConnection class doesn't keep track of
cookies. When you access a servlet or a JSP from a standalone Java program,
sessions don't work. The Web server tries to send you a cookie, but because
the URLConnection class ignores the cookie, you don't send the cookie
back to the server the next time you make a request.

Because Java's URLConnection mechanism is extensible, you can create
your own custom HttpURLConnection class that supports cookies. You can
then use the URL and URLConnection classes exactly as you normally
do, and suddenly your JSP and servlet sessions will work.

When the Web server sends a cookie to a browser, it sends a request header
called Set-cookie. The format of the header is basically as follows:

Set-cookie: name=value; path=somepath; expires=expiretime

The main part of the cookie is name=value because it specifies
the cookie's value. The path specifies the base path for returning the cookie
to the Web server. When the browser accesses a page on the server, it compares
the page's path to the paths for all cookies from the server. If the cookie's
path is /, the browser sends the cookie in every request to the server.

The expires setting tells the browser how long to hold on to the cookie.
If there is no expiry time, the cookie disappears when you close down the browser.

The following Java class represents a cookie value and has the capability to
parse its data from a Set-cookie header value:

package com.wutka.net.http;
import java.net.*;
import java.util.*;
// This class represents a Netscape cookie. It can parse its
// values from the string from a Set-cookie: response (without
// the Set-cookie: portion, of course). It is little more than
// a fancy data structure.
public class Cookie
{
// Define the standard cookie fields
public String name;
public String value;
public Date expires;
public String domain;
public String path;
public boolean isSecure;
// cookieString is the original string from the Set-cookie header.
// You just save it rather than trying to regenerate for the toString
// method. Note that because this class can initialize itself from this
// string, it can be used to save a persistent copy of this class!
public String cookieString;
// Initialize the cookie based on the origin URL and the cookie string
public Cookie(URL sourceURL, String cookieValue)
{
domain = sourceURL.getHost();
path = sourceURL.getFile();
parseCookieValue(cookieValue);
}
// Initialize the cookie based solely on its cookie string
public Cookie(String cookieValue)
{
parseCookieValue(cookieValue);
}
// Parse a cookie string and initialize the values
protected void parseCookieValue(String cookieValue)
{
cookieString = cookieValue;
// Separate out the various fields which are separated by ;'s
StringTokenizer tokenizer = new StringTokenizer(
cookieValue, ";");
while (tokenizer.hasMoreTokens()) {
// Eliminate leading and trailing whitespace
String token = tokenizer.nextToken().trim();
// See if the field is of the form name=value or if it is just
// a name by itself.
int eqIndex = token.indexOf('=');
String key, value;
// If it is just a name by itself, set the field's value to null
if (eqIndex == -1) {
key = token;
value = null;
// Otherwise, the name is to the left of the '=', value is to the right
} else {
key = token.substring(0, eqIndex);
value = token.substring(eqIndex+1);
}
isSecure = false;
// Convert the key to lowercase for comparison with the standard field names
String lcKey = key.toLowerCase();
if (lcKey.equals("expires")) {
expires = new Date(value);
} else if (lcKey.equals("domain")) {
if (isValidDomain(value)) {
domain = value;
}
} else if (lcKey.equals("path")) {
path = value;
} else if (lcKey.equals("secure")) {
isSecure = true;
// If the key wasn't a standard field name, it must be the cookie's name, so
// you don't use the lowercase version of the name here.
} else {
name = key;
this.value = value;
}
}
}
// isValidDomain performs the standard cookie domain check. A cookie
// domain must have at least two portions if it ends in
// .com, .edu, .net, .org, .gov, .mil, or .int. If it ends in something
// else, it must have 3 portions. In other words, you can't specify
// .com as a domain because it has to be something.com, and you can't specify
// .ga.us as a domain because it has to be something.ga.us.
protected boolean isValidDomain(String domain)
{
// Eliminate the leading period for this check
if (domain.charAt(0) == '.') domain = domain.substring(1);
StringTokenizer tokenizer = new StringTokenizer(domain, ".");
int nameCount = 0;
// Just count the number of names and save the last one you saw
String lastName = "";
while (tokenizer.hasMoreTokens()) {
lastName = tokenizer.nextToken();
nameCount++;
}
// At this point, nameCount is the number of sections of the domain
// and lastName is the last section.
// More than 2 sections is okay for everyone
if (nameCount > 2) return true;
// Less than 2 is bad for everyone
if (nameCount < 2) return false;
// Exactly two, you better match one of these 7 domain types
if (lastName.equals("com") || lastName.equals("edu") ||
lastName.equals("net") || lastName.equals("org") ||
lastName.equals("gov") || lastName.equals("mil") ||
lastName.equals("int")) return true;
// Nope, you fail - bad domain!
return false;
}
// You use the cookie string as originally set in the Set-cookie header
// field as the string value of this cookie. It is unique, and if you write
// this string to a file, you can completely regenerate this object from
// this string, so you can read the cookie back out of a file.
public String toString()
{
return cookieString;
}
}

Parsing the cookie value is only half the battle. You must also figure out
which cookies to send for a particular URL. To send a cookie to a Web server,
you set a request property called Cookie containing the name=value
pairs for every cookie you want to send, separated by semicolons.

The following Java class keeps track of cookie values and, given a URL, generates
a cookie string that you can send back to a Web server.

package com.wutka.net.http;
import java.net.*;
import java.io.*;
import java.util.*;
// This class is used to keep track of all known cookies. It
// is your responsibility to load it when your application starts
// and to save it before you quit. You must also manually insert the
// cookies in the database and check for them when doing a GET.
public class CookieDatabase extends Object
{
protected static Vector cookies;
// Initialize the cookie table from a file
public static void loadCookies(String cookieFile)
throws IOException
{
// If the cookie table hasn't been created, create it
if (cookies == null) {
cookies = new Vector();
}
// Open the file
DataInputStream inStream = new DataInputStream(
new FileInputStream(cookieFile));
String line;
// Read lines from the file and create cookies from the line.
// The lines should have been written using the toString method
// in the Cookie class - that way you can just pass the lines
// to the Cookie constructor.
while ((line = inStream.readLine()) != null) {
Cookie cookie = new Cookie(line);
// Add the cookie to the cookie table
addCookie(cookie);
}
inStream.close();
}
// Save the cookie table to a file
public static void saveCookies(String cookieFile)
throws IOException
{
// If the table isn't here, create it - so you'll create an empty file,
// no big deal, really.
if (cookies == null) {
cookies = new Vector();
}
PrintStream outStream = new PrintStream(
new FileOutputStream(cookieFile));
Enumeration e = cookies.elements();
// Write out every cookie in the table using the cookie's toString method.
while (e.hasMoreElements()) {
Cookie cookie = (Cookie) e.nextElement();
outStream.println(cookie.toString());
}
outStream.close();
}
// add a new cookie to the table. If the cookie's name collides with an
// existing cookie, replace the old one.
public static void addCookie(Cookie cookie)
{
if (cookies == null) {
cookies = new Vector();
}
// Go through the cookie table and see if there are any cookies with
// the same domain name, same name, and same path.
for (int i=0; i < cookies.size(); i++) {
Cookie currCookie = (Cookie) cookies.elementAt(i);
if (!currCookie.domain.equals(
cookie.domain)) continue;
if (currCookie.name.equals(cookie.name) &&
currCookie.path.equals(cookie.path)) {
// Looks like you found a match, so replace the old one with this one
cookies.setElementAt(cookie, i);
return;
}
}
// No duplicates, so it's okay to add this one to the end of the table
cookies.addElement(cookie);
}
// getCookieString does some rather ugly things. First, it finds all the
// cookies that are supposed to be sent for a particular URL. Then
// it sorts them by path length, sending the longest path first (that's
// what Netscape's specs say to do - I'm only following orders).
public static String getCookieString(URL destURL)
{
if (cookies == null) {
cookies = new Vector();
}
// sendCookies will hold all the cookies you need to send
Vector sendCookies = new Vector();
// currDate will be used to prune out expired cookies as you go along
Date currDate = new Date();
for (int i=0; i < cookies.size();) {
Cookie cookie = (Cookie) cookies.elementAt(i);
// See if the current cookie has expired. If so, remove it
if ((cookie.expires != null) && (currDate.after(
cookie.expires))) {
cookies.removeElementAt(i);
continue;
}
// You only increment i if you haven't removed the current element
i++;
// If this cookie's domain doesn't match the URL's host, go to the next one
if (!destURL.getHost().endsWith(cookie.domain)) {
continue;
}
// If the paths don't match, go to the next one
if (!destURL.getFile().startsWith(cookie.path)) {
continue;
}
// Okay, you've determined that the current cookie matches the URL, now
// add it to the sendCookies vector in the proper place (that is, ensure
// that the vector goes from longest to shortest path).
int j;
for (j=0; j < sendCookies.size(); j++) {
Cookie currCookie = (Cookie) sendCookies.
elementAt(j);
// If this cookie's path is longer than the cookie[j], you should insert
// it at position j.
if (cookie.path.length() <
currCookie.path.length()) {
break;
}
}
// If j is less than the array size, j represents the insertion point
if (j < sendCookies.size()) {
sendCookies.insertElementAt(cookie, j);
// Otherwise, add the cookie to the end
} else {
sendCookies.addElement(cookie);
}
}
// Now that the sendCookies array is nicely initialized and sorted, create
// a string of name=value pairs for all the valid cookies
String cookieString = "";
Enumeration e = sendCookies.elements();
boolean firstCookie = true;
while (e.hasMoreElements()) {
Cookie cookie = (Cookie) e.nextElement();
if (!firstCookie) cookieString += "; ";
cookieString += cookie.name + "=" + cookie.value;
firstCookie = false;
}
// Return null if there are no valid cookies
if (cookieString.length() == 0) return null;
return cookieString;
}
}

Now that you have a way to parse cookies and store them, you need a way to
look for cookies automatically whenever you use the URLConnection class.
The URL and URLConnection classes enable you to create your own
implementation of a particular protocol. In this case, however, you want to
use the existing implementation of the HTTP protocolit's far too painful
to implement the protocol yourself.

When the URL class sees a request for a particular protocol, it looks
for a handler for the protocol. It first looks at the java.protocol.handler.pkgs
system property. The property may contain a list of packages, separated by vertical
bars. The class looks for Java classes of the form package.protocol.Handler.
For example, if you specify a package of com.wutka.net and you're trying
to use the http protocol (your URL starts with http), the URL
class looks for a class named com.wutka.net.http.Handler.

If the URL class can't find a handler in any of the specified packages,
it looks in the package sun.net.www.protocol. For example, the default
handler for http is sun.net.www.protocol.http.Handler. Some
Java implementations, including Microsoft's, may use a different default handler.

The following Java class creates an instance of the default http protocol
handler and then creates a special wrapper class that adds cookie functionality
to the connection:

The HttpURLConnection class contains quite a few methodsyou certainly
wouldn't want to implement them all yourself. To add cookie functionality, you
really need to do only two things. First, when you create the connection, examine
the cookie database to see if there are any cookies you need to send. If you
need to send cookies, create a Cookie header value with the list of cookies.

Second, when you read the response from the server, look for Set-cookie
header values in the response. Every method that reads response-oriented data
calls getInputStream first to make sure that the connection object has
read the response. You simply need to override the getInputStream method
so that it checks for cookies before returning the input stream.

The following class adds cookie functionality to an existing HttpURLConnection.
Almost all the methods are simple passthroughs that call corresponding methods
in the underlying connection.

When you want to use these classes, just make sure to add the following setting
to your system properties: java.protocol.handler.pkgs=com.wutka.net.
You should now be able to use sessions when you work with Java Server Pages
and servlets. If you are using a Java applet, you can't set the system property.
You must manually connect the connection object to the HttpURLConnectionWrapper
class.

About the Author

Mark Wutka is the president of Wutka Consulting and specializes in helping
companies get the most out of Java. He has built numerous Java, JSP, and servlet
applications, including several online ordering applications. In a past life,
he was the chief architect on a large, object-oriented distributed system providing
automation for the flight operations division of a major airline; for nine years
he designed and implemented numerous systems in Java, C, C++, and Smalltalk
for that same airline. Mark previously contributed chapters to Special
Edition Using Java 2 Platform and is the author of Special Edition
Using Java Server Pages andServlets and Hacking Java. His
next book, Special Edition Using Java 2 Enterprise Edition, will be available
in April.