Core Java Technologies Tech Tips Tips, Techniques, and Sample Code Welcome to the Core Java Technologies Tech Tips for September 13, 2005. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE). This issue covers: * Cookie Management with CookieHandler * Tech Tips Quiz These tips were developed using Java 2 Platform Standard Edition Development Kit 5.0 (JDK 5.0). You can download JDK 5.0 at http://java.sun.com/j2se/1.5.0/download.jsp. This issue of the Core Java Technologies Tech Tips is written by John Zukowski, president of JZ Ventures, Inc. (http://www.jzventures.com). You can view this issue of the Tech Tips on the Web at http://java.sun.com/developer/JDCTechTips/2005/tt0913.html See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms. For more Java technology content, visit these sites: java.sun.com - The latest Java platform releases, tutorials, and newsletters. java.net - A web forum for collaborating and building solutions together. java.com - Hot games, cool apps -- Experience the power of Java technology. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - COOKIE MANAGEMENT WITH COOKIEHANDLER For the Java platform, access to objects through Uniform Resource Locator (URLs) is managed by a series of protocol handlers. The first part of the URL identifies what protocol to use. For example, if you start a URL with "file:", you can access a resource on your local file system. If you start the URL with "http:" the resource is accessed over the Internet. J2SE 5.0 defines what protocol handlers must be present in the system: http, https, file, ftp, and jar. As part of the implementation of the http protocol handler, J2SE 5.0 adds a CookieHandler. This class exposes how state can be managed in the system through cookies. A cookie is a piece of data stored in a browser's cache. If you visit a web site and then revisit it, the cookie data is used to identify you as a return visitor. Cookies allow state information, such as an online shopping cart, to be remembered. A cookie can be short term, holding data for a single web session, that is, until you shut the browser down, or it can be longer term -- holding data for a week or a year. There is no default handler installed with J2SE 5.0. However, you can register a handler so that an application can remember cookies and send them back for your http connections. Returning to the CookieHandler class, this is an abstract class that has two pairs of related methods. The first pair of methods allows you to discover the current handler installed and to install your own handler: o getDefault() o setDefault(CookieHandler) For applications that have an installed security manager, getting and setting the handler requires special permission. To clear the actual handler, pass in null as the handler. As mentioned earlier, there is no default handler. The second pair of methods allows you to get and set cookies from or to a cookie cache that you manage: o get(URI uri, Map> requestHeaders) o put(URI uri, Map> responseHeaders) The get() method retrieves saved cookies from the cache to add to the requestHeaders. The put() method discovers cookies from the response headers for saving in your cache. This looks simple, and creating the handler actually is simple. But defining the cache takes a little more work. To demonstrate, let's use a custom CookieHandler, the cookie cache, and a test program. Here's what the test program looks like: import java.io.*; import java.net.*; import java.util.*; public class Fetch { public static void main(String args[]) throws Exception { if (args.length == 0) { System.err.println("URL missing"); System.exit(-1); } String urlString = args[0]; CookieHandler.setDefault(new ListCookieHandler()); URL url = new URL(urlString); URLConnection connection = url.openConnection(); Object obj = connection.getContent(); url = new URL(urlString); connection = url.openConnection(); obj = connection.getContent(); } } This program first creates and installs a ListCookieHandler class that will be defined shortly. It then opens a connection to a URL (passed in from the command line), and reads the contents. Then the program opens another connection to a URL, and reads the same contents. When reading the first contents, the response includes cookies that will be saved. The second request includes those saved cookies. Now let's look at how to manage this, initially through the URLConnection class. Resources on the Web are accessible through a URL. After you create the URL, you can get an input or output stream to communicate with the site with the help of the URLConnection class. String urlString = ...; URL url = new URL(urlString); URLConnection connection = url.openConnection(); InputStream is = connection.getInputStream(); // .. read content from stream Part of the information available from the connection might be a series of headers. These depend on the protocol in use. To discover the headers, you can use the URLConnection class. The class has various methods to retrieve header information, these include: o getHeaderFields() - Gets a Map of available fields. o getHeaderField(String name) - Gets header fields by name. o getHeaderFieldDate(String name, long default) - Gets the header field as a date. o getHeaderFieldInt(String name, int default) - Gets the header field as a number. o getHeaderFieldKey(int n) or getHeaderField(int n) - Gets the header field by position. To demonstrate, the following program lists all the headers for a given URL: import java.net.*; import java.util.*; public class ListHeaders { public static void main(String args[]) throws Exception { if (args.length == 0) { System.err.println("URL missing"); } String urlString = args[0]; URL url = new URL(urlString); URLConnection connection = url.openConnection(); Map> headerFields = connection.getHeaderFields(); Set set = headerFields.keySet(); Iterator itor = set.iterator(); while (itor.hasNext()) { String key = itor.next(); System.out.println("Key: " + key + " / " + headerFields.get(key)); } } } The ListHeaders program takes as an argument a URL such as http://java.sun.com, and displays all headers received from the site. Each header is displayed in the form: Key: / [] So if you enter: >> java ListHeaders http://java.sun.com You should see something like the following displayed: Key: Set-Cookie / [SUN_ID=192.168.0.1:269421125489956; EXPIRES=Wednesday, 31- Dec-2025 23:59:59 GMT; DOMAIN=.sun.com; PATH=/] Key: Set-cookie / [JSESSIONID=688047FA45065E07D8792CF650B8F0EA;Path=/] Key: null / [HTTP/1.1 200 OK] Key: Transfer-encoding / [chunked] Key: Date / [Wed, 31 Aug 2005 12:05:56 GMT] Key: Server / [Sun-ONE-Web-Server/6.1] Key: Content-type / [text/html;charset=ISO-8859-1] (Long lines in the result shown above were manually split.) This displays only the headers for that URL, it doesn't display the HTML page located there. Notice that the displayed information includes the web server used by the site and the date and time of the local system. Also notice the two "Set-Cookie" lines. These are the headers related to cookies. The cookies can be saved from the headers, and then sent with the next request. Now let's create a CookieHandler. To do that, you need to implement the two abstract methods of CookieHandler: get() and put(): o public void put( URI uri, Map> responseHeaders) throws IOException o public Map> get( URI uri, Map> requestHeaders) throws IOException Start with the put() method. It saves in a cache any cookies that are included in the response headers. To implement put(), first get the List of "Set-Cookie" headers. This can be expanded to other appropriate headers such as Set-cookie and Set-Cookie2. List setCookieList = responseHeaders.get("Set-Cookie"); After you have the list of cookies, loop through each and store them. If the cookie already exists, replace the existing copy: if (setCookieList != null) { for (String item : setCookieList) { Cookie cookie = new Cookie(uri, item); // Remove cookie if it already exists in cache // New one will replace it for (Cookie existingCookie : cache) { ... } System.out.println("Adding to cache: " + cookie); cache.add(cookie); } } The "cache" here can be anything from a database to a List from the Collections Framework. The Cookie class will be defined later -- it is not a predefined class. Essentially, that is all there is to the put() method. For each cookie in the response headers, the method saves the cookie to the cache. The get() method works in the opposite direction. For each cookie in the cache that is appropriate for the URI, the get() method adds it to the request headers. For multiple cookies, create a comma-delimited list. The get() method returns a map, and so the method takes a Map argument with the existing set of headers. You need to add to that argument the appropriate cookies from cache. However, the argument is an immutable map, and you must return another immutable map. So you must copy the existing map to a working copy and then return an immutable map after you've added to it. To implement the get() method, first look in the cache and get the matching cookies, then remove any expired cookies: // Retrieve all the cookies for matching URI // Put in comma-separated list StringBuilder cookies = new StringBuilder(); for (Cookie cookie : cache) { // Remove cookies that have expired if (cookie.hasExpired()) { cache.remove(cookie); } else if (cookie.matches(uri)) { if (cookies.length() > 0) { cookies.append(", "); } cookies.append(cookie.toString()); } } Again, the Cookie class will be defined shortly. There are two required methods shown here: hasExpired() and matches(). The hasExpired() method reports if a particular cookie has expired. The matches() method reports if a cookie is appropriate for the URI passed to the method. The next part of the get() method should take the created StringBuilder object and put the stringified version of it into an unmodifiable Map, in this case, with the appropriate key: "Cookie": // Map to return Map> cookieMap = new HashMap>(requestHeaders); // Convert StringBuilder to List, store in map if (cookies.length() > 0) { List list = Collections.singletonList(cookies.toString()); cookieMap.put("Cookie", list); } return Collections.unmodifiableMap(cookieMap); Here is the complete CookieHandler definition, with some added printlns to show information at runtime: import java.io.*; import java.net.*; import java.util.*; public class ListCookieHandler extends CookieHandler { // "Long" term storage for cookies, not serialized so only // for current JVM instance private List cache = new LinkedList(); /** * Saves all applicable cookies present in the response * headers into cache. * @param uri URI source of cookies * @param responseHeaders Immutable map from field names to * lists of field * values representing the response header fields returned */ public void put( URI uri, Map> responseHeaders) throws IOException { System.out.println("Cache: " + cache); List setCookieList = responseHeaders.get("Set-Cookie"); if (setCookieList != null) { for (String item : setCookieList) { Cookie cookie = new Cookie(uri, item); // Remove cookie if it already exists // New one will replace for (Cookie existingCookie : cache) { if((cookie.getURI().equals( existingCookie.getURI())) && (cookie.getName().equals( existingCookie.getName()))) { cache.remove(existingCookie); break; } } System.out.println("Adding to cache: " + cookie); cache.add(cookie); } } } /** * Gets all the applicable cookies from a cookie cache for * the specified uri in the request header. * * @param uri URI to send cookies to in a request * @param requestHeaders Map from request header field names * to lists of field values representing the current request * headers * @return Immutable map, with field name "Cookie" to a list * of cookies */ public Map> get( URI uri, Map> requestHeaders) throws IOException { // Retrieve all the cookies for matching URI // Put in comma-separated list StringBuilder cookies = new StringBuilder(); for (Cookie cookie : cache) { // Remove cookies that have expired if (cookie.hasExpired()) { cache.remove(cookie); } else if (cookie.matches(uri)) { if (cookies.length() > 0) { cookies.append(", "); } cookies.append(cookie.toString()); } } // Map to return Map> cookieMap = new HashMap>(requestHeaders); // Convert StringBuilder to List, store in map if (cookies.length() > 0) { List list = Collections.singletonList(cookies.toString()); cookieMap.put("Cookie", list); } System.out.println("Cookies: " + cookieMap); return Collections.unmodifiableMap(cookieMap); } } The last piece of the puzzle is the Cookie class itself. The bulk of the intelligence is in the constructor. You have to parse out bits of information in the constructor, from the uri and header field. The expiration date should be in only one format, but multiple formats come across from popular web sites. There is nothing too difficult here -- simply save the different bits of information here such as cookie path, expires date, and domain. public Cookie(URI uri, String header) { String attributes[] = header.split(";"); String nameValue = attributes[0].trim(); this.uri = uri; this.name = nameValue.substring(0, nameValue.indexOf('=')); this.value = nameValue.substring(nameValue.indexOf('=')+1); this.path = "/"; this.domain = uri.getHost(); for (int i=1; i < attributes.length; i++) { nameValue = attributes[i].trim(); int equals = nameValue.indexOf('='); if (equals == -1) { continue; } String name = nameValue.substring(0, equals); String value = nameValue.substring(equals+1); if (name.equalsIgnoreCase("domain")) { String uriDomain = uri.getHost(); if (uriDomain.equals(value)) { this.domain = value; } else { if (!value.startsWith(".")) { value = "." + value; } uriDomain = uriDomain.substring(uriDomain.indexOf('.')); if (!uriDomain.equals(value)) { throw new IllegalArgumentException( "Trying to set foreign cookie"); } this.domain = value; } } else if (name.equalsIgnoreCase("path")) { this.path = value; } else if (name.equalsIgnoreCase("expires")) { try { this.expires = expiresFormat1.parse(value); } catch (ParseException e) { try { this.expires = expiresFormat2.parse(value); } catch (ParseException e2) { throw new IllegalArgumentException( "Bad date format in header: " + value); } } } } } The other methods in the class just return the stored data or check for expiration: public boolean hasExpired() { if (expires == null) { return false; } Date now = new Date(); return now.after(expires); } public String toString() { StringBuilder result = new StringBuilder(name); result.append("="); result.append(value); return result.toString(); } A "match" shouldn't be found if the cookie expired: public boolean matches(URI uri) { if (hasExpired()) { return false; } String path = uri.getPath(); if (path == null) { path = "/"; } return path.startsWith(this.path); } Note that the Cookie specification requires that a match is done on both the domain and the path. For simplicity, only a path match is checked here. Here is the definition of the entire Cookie class: import java.net.*; import java.text.*; import java.util.*; public class Cookie { String name; String value; URI uri; String domain; Date expires; String path; private static DateFormat expiresFormat1 = new SimpleDateFormat("E, dd MMM yyyy k:m:s 'GMT'", Locale.US); private static DateFormat expiresFormat2 = new SimpleDateFormat("E, dd-MMM-yyyy k:m:s 'GMT'", Locale.US); /** * Construct a cookie from the URI and header fields * * @param uri URI for cookie * @param header Set of attributes in header */ public Cookie(URI uri, String header) { String attributes[] = header.split(";"); String nameValue = attributes[0].trim(); this.uri = uri; this.name = nameValue.substring(0, nameValue.indexOf('=')); this.value = nameValue.substring(nameValue.indexOf('=')+1); this.path = "/"; this.domain = uri.getHost(); for (int i=1; i < attributes.length; i++) { nameValue = attributes[i].trim(); int equals = nameValue.indexOf('='); if (equals == -1) { continue; } String name = nameValue.substring(0, equals); String value = nameValue.substring(equals+1); if (name.equalsIgnoreCase("domain")) { String uriDomain = uri.getHost(); if (uriDomain.equals(value)) { this.domain = value; } else { if (!value.startsWith(".")) { value = "." + value; } uriDomain = uriDomain.substring( uriDomain.indexOf('.')); if (!uriDomain.equals(value)) { throw new IllegalArgumentException( "Trying to set foreign cookie"); } this.domain = value; } } else if (name.equalsIgnoreCase("path")) { this.path = value; } else if (name.equalsIgnoreCase("expires")) { try { this.expires = expiresFormat1.parse(value); } catch (ParseException e) { try { this.expires = expiresFormat2.parse(value); } catch (ParseException e2) { throw new IllegalArgumentException( "Bad date format in header: " + value); } } } } } public boolean hasExpired() { if (expires == null) { return false; } Date now = new Date(); return now.after(expires); } public String getName() { return name; } public URI getURI() { return uri; } /** * Check if cookie isn't expired and if URI matches, * should cookie be included in response. * * @param uri URI to check against * @return true if match, false otherwise */ public boolean matches(URI uri) { if (hasExpired()) { return false; } String path = uri.getPath(); if (path == null) { path = "/"; } return path.startsWith(this.path); } public String toString() { StringBuilder result = new StringBuilder(name); result.append("="); result.append(value); return result.toString(); } } Now that you have all the pieces, you can run the earlier Fetch example: >> java Fetch http://java.sun.com Cookies: {Connection=[keep-alive], Host=[java.sun.com], User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], Content-type=[application/x-www-form-urlencoded], Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]} Cache: [] Adding to cache: SUN_ID=192.168.0.1:235411125667328 Cookies: {Connection=[keep-alive], Host=[java.sun.com], User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], Cookie=[SUN_ID=192.168.0.1:235411125667328], Content-type=[application/x-www-form-urlencoded], Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]} Cache: [SUN_ID=192.168.0.1:235411125667328] (Long lines in the result shown above were manually split.) The lines beginning with "Cache:" show the stored cache. Notice how the get() method is called before the put() method, so stored cookies are not returned immediately. For more information on working with cookies and URL connections, see the Custom Networking trail (http://java.sun.com/docs/books/tutorial/networking/index.html) in The Java Tutorial. This is based on J2SE 1.4, so there is no information yet in the Tutorial on the CookieHandler described here. You can also find a default CookieHandler implementation in the Java SE 6 release (https://mustang.dev.java.net/). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TECH TIPS QUIZ Over the years, the Core Java Technologies Tech Tips have covered a wide variety of Java programming language topics. Here's a short quiz that tests your knowledge of some topics covered in past Tech Tips. You can find the answers at the end of the quiz. 1) Which of the following mechanisms can be used to draw dashed lines? a) Rendering hints b) Strokes c) drawDash() method of Graphics2D d) ConvolveOp 2) How do you sort a List of strings? a) Use the TreeList class b) Pass the List to the TreeMap constructor and iterate through the map c) Call the sort method of Arrays d) Call the sort method of Collections 3) How can you install a generic exception handler to handle all uncaught exceptions? a) You can't b) Override the uncaughtException() method of ThreadGroup c) Overload the uncaughtException() method of ThreadGroup d) Override the uncaughtException() method of Thread 4) What library must you include in your classpath to create a custom doclet or taglet? a) javadoc.jar b) tools.jar c) dt.jar d) doclets.jar 5) Which of the following operations can you not do with the Math class? a) Calculate the base 10 log of a number b) Generate a hashcode for a String c) Calculate the cube root of a Unicode character d) Calculate the Euclidean distance between two points Answers 1) Which of the following mechanisms can be used to draw dashed lines? b) Strokes. The May 20, 2003 Tech Tip "Drawing Dashed Lines with Stroke" (http://java.sun.com/developer/JDCTechTips/2003/tt0520.html#1) explains how to draw dashed lines with the Stroke interface and its BasicStroke implementation. 2) How do you sort a List of strings? d) Call the sort method of Collections. You can learn more in the Tech Tip "Using Collections to Sort and Shuffle a List" (http://java.sun.com/developer/JDCTechTips/2004/tt0716.html#1) The Collections Framework has been explored in many Tech Tips. 3) How can you install a generic exception handler to handle all uncaught exceptions? a) and b). You can't install a generic handler for all exceptions, such as those that happen in the event thread. However, you can install a generic handler for all your threads by overriding the uncaughtException() method of ThreadGroup. See the Tech Tip "Handling Uncaught Exceptions" (http://java.sun.com/developer/JDCTechTips/2001/tt0109.html#handling) for more details. 4) What library must you include in your classpath to create a custom doclet or taglet? b) tools.jar. You can learn about creating a custom taglet in the July 22, 2003 Tech Tip "Generating Custom Taglets" (http://java.sun.com/developer/JDCTechTips/2003/tt0722.html#1. Learn about creating doclets in the May 20, 2003 Tech Tip "Generating Custom Doclets" (http://java.sun.com/developer/JDCTechTips/2003/tt0520.html#2). 5) Which of the following operations can you not do with the Math class? b) Generate a hashcode for a String. Calculating the hashcode of a String is the job of the String class. The other three operations can be performed by Math class methods that were added in JDK 5.0. To calculate the cube root of a Unicode character, the character is converted from a char to an int before the calculation is done. See the November 2, 2004 Tech Tip "What's New in the Math Class" (http://java.sun.com/developer/JDCTechTips/2004/tt1102.html#1) for further information. . . . . . . . . . . . . . . . . . . . . . . . IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies: http://www.sun.com/share/text/termsofuse.html http://www.sun.com/privacy/ http://developers.sun.com/dispatcher.jsp?uid=6910008 * FEEDBACK Comments? Please enter your feedback on the Tech Tips at: http://developers.sun.com/contact/feedback.jsp?category=sdn * SUBSCRIBE/UNSUBSCRIBE Subscribe to other Java developer Tech Tips: - Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE). - Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME). To subscribe to these and other JDC publications: - Go to the Sun Developer Network - Subscriptions page, (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), choose the newsletters you want to subscribe to and click "Submit". - To unsubscribe, go to the Subscriptions page, (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), uncheck the appropriate checkbox, and click "Submit". - To use our one-click unsubscribe facility, see the link at the end of this email: - ARCHIVES You'll find the Core Java Technologies Tech Tips archives at: http://java.sun.com/developer/TechTips/index.html - COPYRIGHT Copyright 2005 Sun Microsystems, Inc. All rights reserved. 4150 Network Circle, Santa Clara, California 95054 USA. This document is protected by copyright. For more information, see: http://java.sun.com/developer/copyright.html Core Java Technologies Tech Tips September 13, 2005 Trademark Information: http://www.sun.com/suntrademarks/ Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.