Sessions, Cookies, and URLConnections
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 protocolit'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:
package com.wutka.net.http; import java.net.*; import java.io.*; /** Handler for HTTP URL connections. Instantiates the real handler * and the cookie-handling wrapper class. */ public class Handler extends URLStreamHandler { public Handler() { } public URLConnection openConnection(URL url) throws IOException { // See if the user has specified any proxy settings String proxyHost = System.getProperty("http.proxyHost"); int proxyPort = -1; try { proxyPort = Integer.parseInt( System.getProperty("http.proxyPort", "-1")); } catch (Exception exc) { throw new IOException("Error parsing http.proxyPort: "+ exc.toString()); } // Create the real connection using the built-in HTTP connection class URLConnection conn = new sun.net.www.protocol.http.HttpURLConnection(url, proxyHost, proxyPort); // Create the wrapper class for retrieving cookies from the response return new HttpURLConnectionWrapper(url, (HttpURLConnection) conn); } }
The HttpURLConnection class contains quite a few methodsyou 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.
package com.wutka.net.http; import java.net.*; import java.io.*; import java.security.*; public class HttpURLConnectionWrapper extends HttpURLConnection { protected HttpURLConnection impl; protected InputStream inputStream; public HttpURLConnectionWrapper(URL url, HttpURLConnection handler) { super(url); impl = handler; inputStream = null; // See if there are any cookies for this URL String cookieString = CookieDatabase.getCookieString(url); if (cookieString != null) { // If there are any cookies, store them in the request impl.setRequestProperty("Cookie", cookieString); } } /** Looks for cookies in the response before returning the input stream */ public InputStream getInputStream() throws IOException { // Only look for cookies the first time if (inputStream == null) { inputStream = impl.getInputStream(); processCookies(); } return inputStream; } public void processCookies() { int i = 1; String headerFieldKey = getHeaderFieldKey(i); // Loop through the various header fields. There can be multiple // set-cookie header fields, but the URLConnection class can access only // a single one by name. You must loop through the headers by index number // instead of by name. while (headerFieldKey != null) { // If the header field is a cookie, parse it and add it to the database if (headerFieldKey.equalsIgnoreCase("set-cookie")) { String headerField = getHeaderField(i); Cookie cookie = new Cookie(url, headerField); CookieDatabase.addCookie(cookie); } i++; headerFieldKey = getHeaderFieldKey(i); } } // The rest of the routines simply call methods in the real implementation public void connect() throws IOException { impl.connect(); } public boolean getAllowUserInteraction() { return impl.getAllowUserInteraction(); } public Object getContent() throws IOException { return impl.getContent(); } // Include this for JDK 1.3 support // public Object getContent(Class[] classes) throws IOException // { return impl.getContent(classes); } public String getContentEncoding() { return impl.getContentEncoding(); } public int getContentLength() { return impl.getContentLength(); } public String getContentType() { return impl.getContentType(); } public long getDate() { return impl.getDate(); } public boolean getDefaultUseCaches() { return impl.getDefaultUseCaches(); } public boolean getDoInput() { return impl.getDoInput(); } public boolean getDoOutput() { return impl.getDoOutput(); } public long getExpiration() { return impl.getExpiration(); } public String getHeaderField(int n) { return impl.getHeaderField(n); } public String getHeaderField(String n) { return impl.getHeaderField(n); } public long getHeaderFieldDate(String n, long defaultValue) { return impl.getHeaderFieldDate(n, defaultValue); } public int getHeaderFieldInt(String n, int defaultValue) { return impl.getHeaderFieldInt(n, defaultValue); } public String getHeaderFieldKey(int n) { return impl.getHeaderFieldKey(n); } public long getIfModifiedSince() { return impl.getIfModifiedSince(); } public long getLastModified() { return impl.getLastModified(); } public OutputStream getOutputStream() throws IOException { return impl.getOutputStream(); } public Permission getPermission() throws IOException { return impl.getPermission(); } public String getRequestProperty(String key) { return impl.getRequestProperty(key); } public URL getURL() { return impl.getURL(); } public boolean getUseCaches() { return impl.getUseCaches(); } public void setAllowUserInteraction(boolean allowUserInteraction) { impl.setAllowUserInteraction(allowUserInteraction); } public void setDefaultUseCaches(boolean defaultUseCaches) { impl.setDefaultUseCaches(defaultUseCaches); } public void setDoInput(boolean doInput) { impl.setDoInput(doInput); } public void setDoOutput(boolean doOutput) { impl.setDoOutput(doOutput); } public void setIfModifiedSince(long ifModifiedSince) { impl.setIfModifiedSince(ifModifiedSince); } public void setRequestProperty(String key, String value) { impl.setRequestProperty(key, value); } public void setUseCaches(boolean useCaches) { impl.setUseCaches(useCaches); } public String toString() { return impl.toString(); } public void disconnect() { impl.disconnect(); } public InputStream getErrorStream() { return impl.getErrorStream(); } public String getRequestMethod() { return impl.getRequestMethod(); } public int getResponseCode() throws IOException { return impl.getResponseCode(); } public String getResponseMessage() throws IOException { return impl.getResponseMessage(); } public void setRequestMethod(String method) throws ProtocolException { impl.setRequestMethod(method); } public boolean usingProxy() { return impl.usingProxy(); } }
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 and Servlets and Hacking Java. His next book, Special Edition Using Java 2 Enterprise Edition, will be available in April.