content: /* The contents of this file are subject to the terms
   * of the Common Development and Distribution License
   * (the License). You may not use this file except in
   * compliance with the License.
   *
   * You can obtain a copy of the License at
   * http://www.sun.com/cddl/cddl.html or
   * install_dir/legal/LICENSE
   * See the License for the specific language governing
   * permission and limitations under the License.
   *
   * When distributing Covered Code, include this CDDL
   * Header Notice in each file and include the License file
   * at install_dir/legal/LICENSE.
   * If applicable, add the following below the CDDL Header,
   * with the fields enclosed by brackets [] replaced by
   * your own identifying information:
   * "Portions Copyrighted [year] [name of copyright owner]"
   *
   * $Id$
   *
   * Copyright 2005-2009 Sun Microsystems Inc. All Rights Reserved
   */
  package com.sun.faban.driver.transport.sunhttp;
  
  import com.sun.faban.driver.FatalException;
  
  import java.io.BufferedReader;
  import java.io.FileReader;
  import java.io.IOException;
  import java.net.*;
  import java.text.ParseException;
  import java.text.SimpleDateFormat;
  import java.util.*;
  import java.util.logging.Logger;
  import java.util.logging.Level;
  
  /**
   * ThreadCookieHandler stores cookies from a response and retrieves all applicable
   * cookies for a particular request. On retrieval, the cookies will be
   * formatted as required. If a cookie does not specify a version when it is
   * received - it is non-conformant to IEEE 2965 and 2109 - it is considered
   * a cookie according to Netscape's original proposal. Such cookies will
   * be formatted according to Netscape's proposal. A cookie that specifies
   * a version is considered to follow IEEE2965 and 2109 even if version is 0.
   * Such cookies will be formatted according to IEEE2965. In all cases, the
   * selection rules specified in IEEE2965 applies.<p>
   *
   * Note that the interface of this cookie handler does not comply with
   * java.net.ThreadCookieHandler.
   *
   * @author Akara Sucharitakul
   */
  public class ThreadCookieHandler {
  
      private static InheritableThreadLocal<ThreadCookieHandler> localRef =
              new InheritableThreadLocal<ThreadCookieHandler>();
  
      TreeMap<Integer, CookieStore> cookieStore =
              new TreeMap<Integer, CookieStore>();
  
      int sequence = 0; // Used for ordering the cookies
  
      static Logger logger = Logger.getLogger(
              ThreadCookieHandler.class.getName());
  
      SimpleDateFormat dateParser = null; // Used for parsing "Expires"
  
      /**
       * Creates a new instance of the thread cookie handler for this thread.
       * @return The thread's cookie handler
       */
      public static ThreadCookieHandler newInstance() {
          ThreadCookieHandler handler = new ThreadCookieHandler();
          localRef.set(handler);
          return handler;
      }
  
      /**
       * Obtains the thread cookie handler for this thread.
       * @return The thread's cookie handler
       */
      public static ThreadCookieHandler getInstance() {
          ThreadCookieHandler instance = localRef.get();
          if (instance == null) {
  			throw new FatalException(
                      "Cookie handler not initialized for thread.");
  		}
          return instance;
      }
  
      private ThreadCookieHandler() {
      	super();
      }
  
      /**
       * Puts the cookies from the response headers into the cookie store.
       * @param request The URI of the request
       * @param responseHeaders The response headers.
       */
      public void put(URI request,
                      Map<String, List<String>> responseHeaders) {
          // The main cookie store has domain/host name as key and another
          // map of each individual cookie as the value.
          for (Iterator<String> iter = responseHeaders.keySet().iterator();
               iter.hasNext();) {
              String headerName = iter.next();
              if ("Set-Cookie".equalsIgnoreCase(headerName)) {
                  List<String> cookieList = responseHeaders.get(headerName);
                  // Apparently, the list contains cookies in the reverse order
                  // so we also need to traverse it in the reverse order.
                  for (int i = cookieList.size() - 1; i >= 0; i--) {
                      Cookie cookie = parseAndPutCookie(cookieList.get(i),
                              request, headerName);
                      cookie.type = 1;
                  }
              } else if ("Set-Cookie2".equalsIgnoreCase(headerName)) {
                  List<String> cookieList = responseHeaders.get(headerName);
                  for (int i = cookieList.size() - 1; i >= 0; i--) {
                      Cookie cookie = parseAndPutCookie(cookieList.get(i),
                              request, headerName);
                      cookie.type = 2;
                  }
              }
          }
      }
  
      private Cookie parseAndPutCookie(String cookieString, URI request,
                                       String headerName) {
          Cookie cookie = Cookie.parseCookie(cookieString, this);
          cookie.validate(request);
          putCookies(cookie);
          logger.finer("ResponseHeader - " + headerName + ": "
                  + cookieString);
          return cookie;
      }
  
      /**
       * Selects the cookies applicable to the request URI from the cookie
       * store and puts them into the request header. The cookies are ordered
       * as specified in RFE 2965.
       * @param request The request URI
       * @param requestHeaders The request header map
       * @return The request header map with the cookies put in
       */
      @SuppressWarnings("boxing")
  	public Map<String, List<String>> get(URI request, Map<String,
                                            List<String>> requestHeaders) {
  
          Map<String, List<String>> cookieHeaders =
                  new LinkedHashMap<String, List<String>>();
          cookieHeaders.putAll(requestHeaders);
          for (Map.Entry<Integer, CookieStore> entry : cookieStore.entrySet()) {
              int version = entry.getKey();
              CookieStore store = entry.getValue();
              List<Cookie> cookieList = store.select(request);
              if (cookieList == null) {
  				continue;
  			}
              if (version == -1) {
  				formatNetscapeCookies(cookieList, cookieHeaders);
  			} else {
  				format2965Cookies(cookieList, cookieHeaders);
  			}
          }
          return cookieHeaders;
      }
  
      private static String parseDomain(String hostName) {
          String domainName = null;
          int idx = hostName.indexOf('.', 1);
          if (idx > 0 && idx < hostName.length() - 1) {
  			domainName = hostName.substring(idx);
  		}
          return domainName;
      }
  
      /**
       * Adds the applicable cookies from the cookie handler to the request
       * header of the connection.
       * @param c The connection
       * @throws URISyntaxException 
       */
      public void addRequestCookies(java.net.HttpURLConnection c)
              throws URISyntaxException {
          Map<String, List<String>> requestProps =
                  new LinkedHashMap<String, List<String>>();
          get(c.getURL().toURI(), requestProps);
          for (Iterator<String> iter = requestProps.keySet().iterator();
               iter.hasNext();) {
              String key = iter.next();
              List<String> valueList = requestProps.get(key);
              for (int i = 0; i < valueList.size(); i++) {
                  String value = valueList.get(i);
                  c.addRequestProperty(key, value);
                  logger.finer("RequestHeader - " + key + ": " + value);
              }
          }
      }
  
      /**
       * Obtains the value of all cookies in the cookie store by the name of the
       * cookie.
       * @param name The cookie name
       * @return The values of all cookies matching this name
       */
      public String[] getCookieValuesByName(String name) {
          HashSet<String> valueSet = new HashSet<String>();
          for (CookieStore store : cookieStore.values()) {
              store.getValuesByName(name, valueSet);
          }
          String[] values = new String[valueSet.size()];
          return valueSet.toArray(values);
      }
  
      /**
       * Format cookies according to IETF RFE2965. Note that all cookies in the
       * collection must be of the same version.
       * @param cookies The collection of cookies to format
       * @param requestHeaders The request headers to put the cookie out
       */
      private static void format2965Cookies(Collection<Cookie> cookies,
                                     Map<String, List<String>> requestHeaders) {
  
          StringBuilder b = null;
          for (Cookie cookie : cookies) {
              // We only support cookie version 1
              if (b == null) { // First time initialization
                  if (cookie.version > 1 &&
                          requestHeaders.get("Cookie2") == null) {
                      List<String> cookie2 = new ArrayList<String>(1);
                      cookie2.add("$Version=\"1\"");
                      requestHeaders.put("Cookie2", cookie2);
                  }
                  b = new StringBuilder();
                  b.append("$Version=\"");
                  b.append(cookie.version);
                  b.append('"');
              }
              b.append("; ");
              b.append(cookie.name);
              b.append('=');
              b.append(cookie.value);
  
              if (cookie.pathString != null) {
                  b.append("; $Path=");
                  b.append(cookie.path);
              }
              if (cookie.domainString != null) {
                  b.append("; $Domain=");
                  b.append(cookie.domain);
              }
              if (cookie.portString != null) {
                  b.append("; $Port=");
                  b.append(cookie.portString);
              }
          }
          if (b != null) {
              List<String> list = requestHeaders.get("Cookie");
              if (list == null) {
                  list = new ArrayList<String>();
                  requestHeaders.put("Cookie", list);
              }
              String cookieString = b.toString();
              list.add(cookieString);
              logger.finest("Cookie:" + cookieString);
          } else {
              logger.finer("No request cookies");
          }
      }
  
      /**
       * Format cookies according to Netscape's origina proposal.
       * Note that all cookies in the collection must be of the same version.
       * @param cookies The collection of cookies to format
       * @param requestHeaders The request headers to put the cookie out
       */
      private static void formatNetscapeCookies(Collection<Cookie> cookies,
                                 Map<String, List<String>> requestHeaders) {
  
          StringBuilder b = null;
          for (Cookie cookie : cookies) {
              if (b == null) {
  				b = new StringBuilder();
  			} else {
  				b.append("; ");
  			}
              b.append(cookie.name);
              b.append('=');
              b.append(cookie.value);
          }
          if (b != null) {
              List<String> list = requestHeaders.get("Cookie");
              if (list == null) {
                  list = new ArrayList<String>();
                  requestHeaders.put("Cookie", list);
              }
              String cookieString = b.toString();
              list.add(cookieString);
              logger.finest("Cookie:" + cookieString);
          } else {
              logger.finer("No request cookies");
          }
      }
  
      /**
       * Puts the cookie in the store corresponding to the cookie version.
       * @param cookie The cookie to put in.
       */
      private void putCookies(Cookie cookie) {
          CookieStore store = cookieStore.get(new Integer(cookie.version));
          if (store == null) {
              store = new CookieStore(cookie.version);
              cookieStore.put(new Integer(cookie.version), store);
          }
          store.add(cookie, this);
      }
  
      /**
       * Object structure corresponding to the cookie itself.
       */
      static class Cookie {
          int type;       // 1 or 2 for Cookie or Cookie2
          String name;
          String value;
          String comment;
          String commentURL;  // Set-Cookie2 only
          int discard = -1;    // Set-Cookie2 only
          String domain;
          String domainString;
          long timeStamp;
          int maxAge = -1;
          String path;
          private char[] cPath;   // Used for context-sensitive comparison
          String pathString;  // Original path string
          int[] ports;        // Set-Cookie2 only
          String portString;  // Set-Cookie2 only
          boolean secure;
          int version = -1;   // -1 means not set for the netscape case
  
          /**
           * Parses the cookie as represented in the response header into a
           * cookie object.
           * @param cookieString The string from the response header
           * @param h The instance of the ThreadCookieHandler to parse and store
           * @return The resulting cookie
           */
          static Cookie parseCookie(String cookieString, ThreadCookieHandler h) {
  
              StringTokenizer tokenizer = new StringTokenizer(
                      cookieString, ";", false);
              String nameValue = tokenizer.nextToken();
              int idx = nameValue.indexOf('=');
              if (idx == -1) {
  				return null;
  			}
  
              // Determine the name
              String token = nameValue.substring(0, idx).trim();
              if (token.startsWith("$")) {
  				return null;
  			}
              Cookie cookie = new Cookie();
              cookie.timeStamp = System.currentTimeMillis();
              cookie.name = token;
  
              // Value
              cookie.value = nameValue.substring(idx + 1).trim();
  
              // Now deal with all attributes
              while (tokenizer.hasMoreTokens()) {
                  nameValue = tokenizer.nextToken();
  
                  // Extract the attributes
                  idx = nameValue.indexOf('=');
                  if (idx != -1) {
                      token = nameValue.substring(idx + 1).trim();
                      nameValue = nameValue.substring(0, idx).trim();
                  } else {
                      nameValue = nameValue.trim();
                      token = null;
                  }
  
                  if ("Comment".equalsIgnoreCase(nameValue)) {
                      cookie.comment = token;
                  } else if ("CommentURL".equalsIgnoreCase(nameValue)) {
                      cookie.commentURL = token;
                  } else if ("Discard".equalsIgnoreCase(nameValue)) {
                      cookie.discard = 1;
                  } else if ("Domain".equalsIgnoreCase(nameValue)) {
                      cookie.domainString = token;
                      if (token.startsWith("\"") && token.endsWith("\"")) {
  						cookie.domain = token.substring(1,
                                  token.length() - 1).trim();
  					} else {
  						cookie.domain = token;
  					}
  
                      if (!cookie.domain.startsWith(".")) {
  						cookie.domain = "." + cookie.domain;
  					}
                  } else if ("Expires".equalsIgnoreCase(nameValue)) {
                      // Old Netscape cookie spec, we translate to maxAge
                      long expires = -1;
                      if (h.dateParser == null) {
                          // The Netscape cookie spec takes this format:
                          // "EEE, d-MMM-yyyy HH:mm:ss z" but we've seen cases of
                          // "EEE, d MMM yyyy HH:mm:ss z". Due to ease of
                          // conversion, we replace the '-' with ' ' and parse
                          // the format without the '-'.
  						h.dateParser = new SimpleDateFormat(
                                  "EEE, d MMM yyyy HH:mm:ss z");
  					}
                      try {
                          if (token.startsWith("\"") && token.endsWith("\"")) {
                              token = token.substring(1, token.length() - 1).
                                              trim();
  						}
                          token = token.replace('-', ' ');
                          expires = h.dateParser.parse(token).getTime();
                      } catch (ParseException e) {
                          throw new IllegalArgumentException(e);
                      }
                      long maxAge = expires - cookie.timeStamp;
                      if (maxAge <= 0) {
                          cookie.maxAge = 0;
                      } else {
                          int remainder = (int) (maxAge % 1000);
                          maxAge /= 1000;
                          if (remainder > 0) {
  							++maxAge;
  						}
                          cookie.maxAge = (int) maxAge;
                      }
                  } else if ("Max-Age".equalsIgnoreCase(nameValue)) {
                      if (token.startsWith("\"") && token.endsWith("\"")) {
  						cookie.maxAge = Integer.parseInt(token.substring(1,
                                  token.length() - 1).trim());
  					} else {
  						cookie.maxAge = Integer.parseInt(token);
  					}
  
                  } else if ("Path".equalsIgnoreCase(nameValue)) {
                      cookie.pathString = token;
                      if (token.startsWith("\"") && token.endsWith("\"")) {
  						cookie.path = token.substring(1,
                                  token.length() - 1).trim();
  					} else {
  						cookie.path = token;
  					}
                  } else if ("Port".equalsIgnoreCase(nameValue)) {
                      // Strip off the quotes
                      if (token == null) { // Port attribute without value
                          cookie.ports = new int[1];
                          // Leave it null for now. Validate will fill this in.
                          continue;
                      }
  
                      cookie.portString = token; // Save as is for sending.
                      if (token.startsWith("\"") && token.endsWith("\"")) {
  						token = token.substring(1,
                                  token.length() - 1).trim();
  					}
  
                      // Put all ports into the list
                      ArrayList<String> portList = new ArrayList<String>();
                      for (StringTokenizer t = new StringTokenizer(
                              token, ", ", false); t.hasMoreTokens();
                           portList.add(t.nextToken())) {
  						// do nothing, the loops does the rest
  					}
  
                      // Allocate array and parse all ports in the array
                      cookie.ports = new int[portList.size()];
                      for (int i = 0; i < cookie.ports.length; i++) {
  						cookie.ports[i] = Integer.parseInt(portList.get(i));
  					}
                  } else if ("Secure".equalsIgnoreCase(nameValue)) {
                      cookie.secure = true;
                  } else if ("Version".equalsIgnoreCase(nameValue)) {
                      if (token.startsWith("\"") && token.endsWith("\"")) {
  						cookie.version = Integer.parseInt(token.substring(1,
                                  token.length() - 1).trim());
  					} else {
  						cookie.version = Integer.parseInt(token);
  					}
                      if (cookie.version < 0) {
  						throw new IllegalArgumentException("Cookie version " +
                                  "must be greater than 0, received " +
                                  cookie.version + '.');
  					}
                  }
              }
              return cookie;
          }
  
          /**
           * Minimally validates the cookie. Note that this implementation is
           * for a benchmark driver. We only do minimal validation and no
           * cookie spoofing/security checks.
           * @param request The URL of this request
           * @return true if this cookie is valid or is made valid.
           */
          boolean validate(URI request) {
  
              if (domain == null) {
                  domain = request.getHost();
                  logger.finest("Set cookie domain to " + domain);
              }
  
              if (path == null) {
                  path = request.getPath();
                  int idx = path.lastIndexOf('/');
                  path = path.substring(0, idx + 1);
                  logger.finest("Set cookie path to " + path);
              }
  
  
              if (discard == -1) {// unset
                  if (maxAge == -1) {
  					discard = 1; // discards on exit
  				} else {
  					discard = 0; // if max-time set, don't discard
  				}
              }
  
              // Check for case of ports set without attribute.
              if (portString == null && ports != null) {
                  ports[0] = request.getPort();
                  if (ports[0] == -1) {
  					ports[0] = 80; // HTTP default port
  				}
              }
              return true;
          }
  
          /**
           * Matches a cookie's path to a request path to decide to send
           * or not to send cookie.
           * @param requestPath The URI path of the request
           * @return true if the request path matches this cookie, false otherwise
           */
          boolean matchPath(String requestPath) {
              if (cPath == null) // Initialize cPath for multiple uses.
                  cPath = path.toCharArray();
  
              char[] rPath = requestPath.toCharArray(); // request path
              boolean[] mark = new boolean[2]; // boolean array saves space.
  
              for (int i = 0, j = 0; i < cPath.length; i++, j++) {
  
                  if (j >= rPath.length) // request path too short to match
                      return false;
  
                  if (cPath[i] == '/')
                      if (mark[0]) {
                          ++i;
                          while (i < cPath.length && cPath[i] == '/')
                              ++i;
                          if (i == cPath.length)
                              break;
                          mark[0] = false;
                      } else {
                          mark[0] = true;
                      }
                  else
                      mark[0] = false;
  
                  if (rPath[j] == '/')
                      if (mark[1]) {
                          ++j;
                          while (j < rPath.length && rPath[j] == '/')
                              ++j;
                          if (j == rPath.length) // too short to match
                              return false;
                          mark[1] = false;
                      } else {
                          mark[1] = true;
                      }
                  else
                      mark[1] = false;
  
                  if (cPath[i] != rPath[j])
                      return false;
              }
              return true;
          }
  
          /**
           * A cookie is equal to another cookie and replaces the other cookie
           * if for the same domain or request it has the same name and the same
           * path. Cookies that do not share the effective host/domain may not
           * be compared.
           * @param o The object to compare to
           * @return True if it is equivalent, false otherwise
           */
          @Override
  		public boolean equals(Object o) {
              if (o instanceof Cookie) {
                  Cookie other = (Cookie) o;
                  if (name.equals(other.name) && path.equals(other.path)) {
  					return true;
  				}
              }
              return false;
          }
  
          /**
           * The hashCode of a cookie is represented from the equivalency
           * standpoint. That is, if two cookies have the same name and path,
           * they are considered equivalent within the effective host's domain.
           * Cookies that do not share the effective host/domain may not
           * be compared.
           * @return The hash code of this cookie
           */
          @Override
  		public int hashCode() {
              return (name + path).hashCode();
          }
  
          /**
           * Returns a string representation of the object. In general, the
           * <code>toString</code> method returns a string that
           * "textually represents" this object. The result should
           * be a concise but informative representation that is easy for a
           * person to read.
           * It is recommended that all subclasses override this method.
           * <p/>
           * The <code>toString</code> method for class <code>Object</code>
           * returns a string consisting of the name of the class of which the
           * object is an instance, the at-sign character `<code>@</code>', and
           * the unsigned hexadecimal representation of the hash code of the
           * object. In other words, this method returns a string equal to the
           * value of:
           * <blockquote>
           * <pre>
           * getClass().getName() + '@' + Integer.toHexString(hashCode())
           * </pre></blockquote>
           *
           * @return a string representation of the object.
           */
          @Override public String toString() {
              StringBuilder b = new StringBuilder();
              b.append(name).append('=').append(value).append(", domain: ");
              b.append(domain).append(", path: ").append(path).
                      append(", ports: ").append(ports);
              return b.toString();
          }
      }
  
      /**
       * Stores a cookie of a particular version, searchable by domain.
       */
      static class CookieStore implements Comparable<CookieStore> {
  
          int version;
  
          Map<String, DomainCookieStore> store =
                  new HashMap<String, DomainCookieStore>();
  
          /**
           * Constructs the cookie store.
           * @param version The cookie version being used
           */
          public CookieStore(int version) {
              this.version = version;
          }
  
          /**
           * Adds the cookie to the applicable domain.
           * @param cookie The cookie
           * @param h the ThreadCookieHandler to add the cookie
           */
          void add(Cookie cookie, ThreadCookieHandler h) {
              DomainCookieStore dStore = store.get(cookie.domain);
              if (dStore == null) {
                  dStore = new DomainCookieStore(cookie.domain);
                  store.put(cookie.domain, dStore);
              }
              dStore.add(cookie, h);
          }
  
          /**
           * Selects the cookies applicable to the given URL.
           * @param request The URL
           * @return A list of applicable cookies
           */
          List<Cookie> select(URI request) {
  
              // Figure out the applicable domains
              InetAddress[] address = null;
              String hostName = request.getHost();
              
              if (hostName == null) {
                  logger.warning("Request " + request + " does not contain a " +
                                 "hostname or has an invalid hostname.");
              }
  
              try {
                  address = InetAddress.getAllByName(hostName);
              } catch (UnknownHostException e) {
                  return null;
              }
  
              HashSet<String> domainSet = new HashSet<String>();
  
              String domain = hostName;
              while (domain != null) {
                  logger.finest("Select(0) cookie, domain: " + domain);
                  domainSet.add(domain);
                  domain = parseDomain(domain);
              }
  
              domain = address[0].getCanonicalHostName();
              while (domain != null) {
                  logger.finest("Select(1) cookie, domain: " + domain);
                  domainSet.add(domain);
                  domain = parseDomain(domain);
              }
  
              domain = address[0].getHostName();
              while (domain != null) {
                  logger.finest("Select(2) cookie, domain: " + domain);
                  domainSet.add(domain);
                  domain = parseDomain(domain);
              }
  
              for (int i = 0; i < address.length; i++) {
                  String addr = address[i].getHostAddress();
                  logger.finest("Select(3) cookie, host: " + addr);
                  domainSet.add(addr);
              }
  
              // Merge result for applicable domains
              DomainCookieStore dStore = null;
              for (String dm : domainSet) {
                  DomainCookieStore dxStore = store.get(dm);
                  logger.finest("Fetching cookies for domain " + dm);
                  if (dxStore == null) {
                      logger.finer("No cookies found for domain " + dm);
                      continue;
                  }
                  if (dStore == null) {
  					dStore = dxStore.select(request);
  				} else {
  					dStore.merge(dxStore.select(request));
  				}
              }
  
              if (dStore == null) {
                  logger.finer("No cookies found for request " + request);
  				return null;
  			}
  
              return dStore.getSortedCookieList();
          }
  
          void getValuesByName(String name, Collection<String> c) {
              for (DomainCookieStore dStore : store.values()) {
  				dStore.getValuesByName(name, c);
  			}
          }
  
          /**
           * Compares this cookie store to another cookie store.
           * @param s The other cookie store
           * @return Positive, negative integer or 0 based on the comparison
           */
          public int compareTo(CookieStore s) {
              return s.version - version;
          }
  
          /**
           * Tests the cookis store for equivalence.
           * @param o The other object
           * @return True if stores are equivalent
           */
          @Override
  		public boolean equals(Object o) {
              if (o instanceof CookieStore) {
                  CookieStore s = (CookieStore) o;
                  return version == s.version;
              }
              return false;
          }
      }
  
      /**
       * Stores the cookies for a particular domain, separated by name.
       */
      static class DomainCookieStore {
          String domain;
          Map<String, NameCookieStore> store =
                  new HashMap<String, NameCookieStore>();
  
          /**
           * Creates a cookie store for the given domain.
           * @param domain The domain this store is applicable to.
           */
          DomainCookieStore(String domain) {
              this.domain = domain;
          }
  
          /**
           * Adds a cookie to this, separated by its name.
           * @param cookie The cookie
           * @param h The thread cookie handler to store
           */
          void add(Cookie cookie, ThreadCookieHandler h) {
              NameCookieStore nStore = store.get(cookie.name);
              if (nStore == null) {
                  nStore = new NameCookieStore(cookie.name, h);
                  store.put(cookie.name, nStore);
              }
              nStore.add(cookie);
          }
  
          /**
           * Selects the cookies in this store applicable to the request.
           * @param request The URL of the request
           * @return Another cookie store containing only cookies applicable
           *         to the request.
           */
          DomainCookieStore select(URI request) {
              DomainCookieStore result = null;
              for (NameCookieStore nStore : store.values()) {
                  NameCookieStore nResult = nStore.select(request);
                  if (nResult != null) {
                      if (result == null) {
  						result = new DomainCookieStore(domain);
  					}
                      result.store.put(nResult.name, nResult);
                  }
              }
              return result;
          }
  
          void getValuesByName(String name, Collection<String> c) {
              NameCookieStore nStore = store.get(name);
              for (Cookie cookie : nStore.store.values()) {
  				c.add(cookie.value);
  			}
          }
  
          /**
           * Merges the domain cookie store with a cookie store of
           * another domain. These two domains are usually applicable to the
           * request.
           * @param other The store of the other domain
           * @return This store
           */
          DomainCookieStore merge(DomainCookieStore other) {
              if (other == null) {
  				return this;
  			}
              if (domain.equals(other.domain)) {
  				throw new IllegalArgumentException(
                          "Cannot merge store with same name.");
  			}
  
              for (NameCookieStore nStore : other.store.values()) {
                  NameCookieStore oldNStore = store.put(nStore.name, nStore);
                  if (oldNStore != null) {
  					nStore.merge(oldNStore);
  				}
              }
              domain += ',' + other.domain;
              return this;
          }
  
          /**
           * Obtains a list of cookies from this store sorted as specified by
           * RFE 2965 and the sequence it is received if the sequence is left
           * undefined in RFE 2965.
           * @return The list of cookies, appropriately sorted
           */
          List<Cookie> getSortedCookieList() {
              TreeSet<NameCookieStore> nStoreSet =
                      new TreeSet<NameCookieStore>(store.values());
              ArrayList<Cookie> list = new ArrayList<Cookie>(nStoreSet.size());
              TreeSet<Cookie> cookieSet =
                      new TreeSet<Cookie>(new CookiePathComparator());
  
              for (NameCookieStore nStore : nStoreSet) {
                  cookieSet.addAll(nStore.store.values());
                  for (Cookie cookie : cookieSet) {
                      list.add(cookie);
                  }
                  cookieSet.clear();
              }
              return list;
          }
      }
  
      /**
       * Stores the cookies for a certain cookie name.
       */
      static class NameCookieStore implements Comparable<NameCookieStore> {
  
          int id;
          String name;
  
          // Cookie path is the key here
          Map<String, Cookie> store = new HashMap<String, Cookie>();
  
          /**
           * Constructs a cookie store for a certain name.
           * @param name The name of the cookie
           * @param h The thread cookie handler to use
           */
          NameCookieStore(String name, ThreadCookieHandler h) {
              this.name = name;
              id = h.sequence++;
          }
  
          /**
           * Constructs a cookie store with a certain sequence id.
           * @param name Name of the cookies to be stored
           * @param id The sequence number of this name, used for sorting
           */
          private NameCookieStore(String name, int id) {
              this.name = name;
              this.id = id;
          }
  
          /**
           * Adds a cookie matching the name of this store to the store.
           * @param cookie The cookie to be added
           */
          void add(Cookie cookie) {
              // Check whether this is actually a remove - maxAge == 0
              if (cookie.maxAge == 0) {
                  store.remove(cookie.path);
                  return;
              }
  
              // Optimistically replace
              Cookie oldCookie = store.put(cookie.path, cookie);
  
              // Then check validity and add back if the old one is newer
              if (oldCookie != null && oldCookie.timeStamp > cookie.timeStamp) {
  				store.put(oldCookie.path,  oldCookie);
  			}
          }
  
          /**
           * Merges the stores and will return this store.
           * No new store is allocated.
           * @param otherStore The store to merge this storte with
           * @return This store after the merge
           */
          public NameCookieStore merge(NameCookieStore otherStore) {
              if (otherStore == null) {
  				return this;
  			}
  
              if (!name.equals(otherStore.name)) {
  				throw new IllegalArgumentException(
                          "Can only merge stores with same name!");
  			}
  
              if (id > otherStore.id) {
  				id = otherStore.id;
  			}
  
              for (Cookie cookie : otherStore.store.values()) {
  				add(cookie);
  			}
  
              return this;
          }
  
          /**
           * Selects the cookies for this name accroding to the rules specified
           * in RFE 2965.
           * @param request The URI of the request
           * @return A name cookie store containing the selected cookies
           */
          public NameCookieStore select(URI request) {
  
              NameCookieStore result = null;
  
              for (Cookie cookie : store.values()) {
                  // 1. Domain selection - the domain was selected by now.
  
                  // 2. Max age selection.
                  // We do this first to purge all timed-out cookies.
                  long currentTime;
                  if (cookie.maxAge != -1 &&
                          (currentTime = System.currentTimeMillis()) >=
                          cookie.maxAge * 1000l + cookie.timeStamp) {
                      logger.fine("Current time: " + currentTime + ", " +
                              "purging timed out cookie " + cookie.name + '=' +
                              cookie.value + ", timestamp: " + cookie.timeStamp +
                              ", ,max-age: " + cookie.maxAge);
                      store.remove(cookie.path);
                      if (store.size() == 0) {
                          return null;
                      }
                  }
  
                  // 3. Port selection
                  int requestPort = request.getPort();
                  if (requestPort == -1) {
  					requestPort = 80; // Default HTTP port
  				}
  
                  boolean portDenied = true;
                  if (cookie.ports == null) {
  					portDenied = false;
  				} else {
  					for (int allowedPort : cookie.ports) {
  						if (requestPort == allowedPort) {
                              portDenied = false;
                              break;
                          }
  					}
  				}
                  if (portDenied) {
                      if (logger.isLoggable(Level.FINEST))
                          logger.finest("Port denied. Cookie: " + cookie +
                                                  ", Request: " + request);
  					continue;
  				}
  
                  // Path selection
                  String path = request.getPath();
                  if (path == null) {
  					path = "/";
  				}
                  if (!cookie.matchPath(path)) {
                      if (logger.isLoggable(Level.FINEST))
                          logger.finest("Path denied. Cookie: " + cookie +
                                                  ", Request path: " + path);
  					continue;
  				}
  
                  if (result == null) {
  					result = new NameCookieStore(name, id);
  				}
  
                  result.add(cookie);
              }
              return result;
          }
  
          /**
           * Compares this store to another store for ordering by id.
           * @param nStore The other store
           * @return A positive integer, a negative integer, or 0
           */
          public int compareTo(NameCookieStore nStore) {
              return id - nStore.id;
          }
      }
  
      /**
       * The comparator used to order the cookies by path.
       */
      static class CookiePathComparator implements Comparator<Cookie> {
  
          /**
           * Coompares the cookie by path for ordering according the RFE 2965.
           * @param c The first cookie
           * @param c1 The second cookie
           * @return The comparison result, either positive, negative integer,
           *         or 0
           */
          public int compare(Cookie c, Cookie c1) {
              return c1.path.length() - c.path.length();
          }
      }
  
      /**
       * Main method to test cookie handler.
       * @param args Name of each file representing each request header
       * @throws IOException Cannot find or read file
       * @throws URISyntaxException 
       */
      public static void main(String[] args)
              throws IOException, URISyntaxException {
          ThreadCookieHandler handler = new ThreadCookieHandler();
          Map<String, List<String>> reqHeader =
                  new HashMap<String, List<String>>();
          Map<String, List<String>> respHeader =
                  new HashMap<String, List<String>>();
          for (int i = 0; i < args.length; i++) {
              BufferedReader r = new BufferedReader(new FileReader(args[i]));
              String cookieHeader = null;
              System.out.println("---- Response Header ----");
              while((cookieHeader = r.readLine()) != null) {
                  System.out.println(cookieHeader);
                  int idx = cookieHeader.indexOf(": ");
                  if (idx == -1) {
  					continue;
  				}
                  String key = cookieHeader.substring(0, idx);
                  String value = cookieHeader.substring(idx + 2);
                  List<String> values = respHeader.get(key);
                  if (values == null) {
                      values = new ArrayList<String>();
                      respHeader.put(key, values);
                  }
                  values.add(value);
              }
              r.close();
              URL url = new URL("http", "sunstorm.sfbay.sun.com", 80, "/uwc");
              handler.put(url.toURI(), respHeader);
              handler.get(url.toURI(), reqHeader);
              System.out.println("---- Request Header ----");
              for (Iterator<String> iter = reqHeader.keySet().iterator();
                   iter.hasNext(); ) {
                  String key = iter.next();
                  List<String> values = reqHeader.get(key);
                  for (int j = 0; j < values.size(); j++) {
  					System.out.println(key + ": " + values.get(j));
  				}
              }
              reqHeader.clear();
          }
      }
  }