/*
 * @(#)CookieModule.java             0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal���r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/ 
 *
 */

package org.exoplatform.common.http.client;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.awt.Button;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ProtocolException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * This module handles Netscape cookies (also called Version 0 cookies) and
 * Version 1 cookies. Specifically is reads the <var>Set-Cookie</var> and
 * <var>Set-Cookie2</var> response headers and sets the <var>Cookie</var> and
 * <var>Cookie2</var> headers as neccessary.
 * <P>
 * The accepting and sending of cookies is controlled by a
 * <var>CookiePolicyHandler</var>. This allows you to fine tune your privacy
 * preferences. A cookie is only added to the cookie list if the handler allows
 * it, and a cookie from the cookie list is only sent if the handler allows it.
 * <P>
 * This module expects to be the only one handling cookies. Specifically, it
 * will remove any <var>Cookie</var> and <var>Cookie2</var> header fields found
 * in the request, and it will remove the <var>Set-Cookie</var> and
 * <var>Set-Cookie2</var> header fields in the response (after processing them).
 * In order to add cookies to a request or to prevent cookies from being sent,
 * you can use the {@link #addCookie(HTTPClient.Cookie) addCookie} and
 * {@link #removeCookie(HTTPClient.Cookie) removeCookie} methods to manipulate
 * the module's list of cookies.
 * <P>
 * A cookie jar can be used to store cookies between sessions. This file is read
 * when this class is loaded and is written when the application exits; only
 * cookies from the default context are saved. The name of the file is
 * controlled by the system property <var>HTTPClient.cookies.jar</var> and
 * defaults to a system dependent name. The reading and saving of cookies is
 * enabled by setting the system property <var>HTTPClient.cookies.save</var> to
 * <var>true</var>.
 * @see <a
 *      href="http://home.netscape.com/newsref/std/cookie_spec.html">Netscape's
 *      cookie spec</a>
 * @see <a href="http://www.ietf.org/rfc/rfc2965.txt">HTTP State Management *
 *      Mechanism spec</a>
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal���r
 * @since V0.3
 */
@SuppressWarnings("deprecation")
public class CookieModule implements HTTPClientModule
{
   /** the list of known cookies */
   private static Hashtable cookie_cntxt_list = new Hashtable();

   /** the file to use for persistent cookie storage */
   private static File cookie_jar = null;

   /** an object, whose finalizer will save the cookies to the jar */
   private static Object cookieSaver = null;

   /** the cookie policy handler */
   private static CookiePolicyHandler cookie_handler = new DefaultCookiePolicyHandler();

   private static final Log LOG = ExoLogger.getLogger("exo.ws.commons.CookieModule");

   // read in cookies from disk at startup

   static
   {
      boolean persist;
      try
      {
         persist = Boolean.getBoolean("HTTPClient.cookies.save");
      }
      catch (Exception e)
      {
         persist = false;
      }

      if (persist)
      {
         loadCookies();

         // the nearest thing to atexit() I know of...

         cookieSaver = new Object()
         {
            @Override
            public void finalize()
            {
               saveCookies();
            }
         };
      }
   }

   private static void loadCookies()
   {
      // The isFile() etc need to be protected by the catch as signed
      // applets may be allowed to read properties but not do IO
      try
      {
         cookie_jar = new File(getCookieJarName());
         if (cookie_jar.isFile() && cookie_jar.canRead())
         {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(cookie_jar));
            cookie_cntxt_list.put(HTTPConnection.getDefaultContext(), ois.readObject());
            ois.close();
         }
      }
      catch (IOException t)
      {
         cookie_jar = null;
      }
      catch (ClassNotFoundException e)
      {
         cookie_jar = null;
      }
   }

   private static void saveCookies()
   {
      if (cookie_jar != null && (!cookie_jar.exists() || cookie_jar.isFile() && cookie_jar.canWrite()))
      {
         Hashtable cookie_list = new Hashtable();
         Enumeration en = Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext()).elements();

         // discard cookies which are not to be kept across sessions

         while (en.hasMoreElements())
         {
            Cookie cookie = (Cookie)en.nextElement();
            if (!cookie.discard())
               cookie_list.put(cookie, cookie);
         }

         // save any remaining cookies in jar
         if (cookie_list.size() > 0)
         {
            try
            {
               ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(cookie_jar));
               oos.writeObject(cookie_list);
               oos.close();
            }
            catch (IOException t)
            {
               if (LOG.isTraceEnabled())
               {
                  LOG.trace("An exception occurred: " + t.getMessage());
               }
            }
         }
      }
   }

   private static String getCookieJarName()
   {
      String file = null;

      try
      {
         file = System.getProperty("HTTPClient.cookies.jar");
      }
      catch (Exception e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }

      if (file == null)
      {
         // default to something reasonable

         String os = System.getProperty("os.name");
         if (os.equalsIgnoreCase("Windows 95") || os.equalsIgnoreCase("16-bit Windows")
            || os.equalsIgnoreCase("Windows"))
         {
            file = System.getProperty("java.home") + File.separator + ".httpclient_cookies";
         }
         else if (os.equalsIgnoreCase("Windows NT"))
         {
            file = System.getProperty("user.home") + File.separator + ".httpclient_cookies";
         }
         else if (os.equalsIgnoreCase("OS/2"))
         {
            file = System.getProperty("user.home") + File.separator + ".httpclient_cookies";
         }
         else if (os.equalsIgnoreCase("Mac OS") || os.equalsIgnoreCase("MacOS"))
         {
            file = "System Folder" + File.separator + "Preferences" + File.separator + "HTTPClientCookies";
         }
         else
         // it's probably U*IX or VMS
         {
            file = System.getProperty("user.home") + File.separator + ".httpclient_cookies";
         }
      }

      return file;
   }

   // Constructors

   CookieModule()
   {
   }

   // Methods

   /**
    * Invoked by the HTTPClient.
    */
   public int requestHandler(Request req, Response[] resp)
   {
      // First remove any Cookie headers we might have set for a previous
      // request

      NVPair[] hdrs = req.getHeaders();
      int length = hdrs.length;
      for (int idx = 0; idx < hdrs.length; idx++)
      {
         int beg = idx;
         while (idx < hdrs.length && hdrs[idx].getName().equalsIgnoreCase("Cookie"))
            idx++;

         if (idx - beg > 0)
         {
            length -= idx - beg;
            System.arraycopy(hdrs, idx, hdrs, beg, length - beg);
         }
      }
      if (length < hdrs.length)
      {
         hdrs = Util.resizeArray(hdrs, length);
         req.setHeaders(hdrs);
      }

      // Now set any new cookie headers

      Hashtable cookie_list = Util.getList(cookie_cntxt_list, req.getConnection().getContext());
      if (cookie_list.size() == 0)
         return REQ_CONTINUE; // no need to create a lot of objects

      Vector names = new Vector();
      Vector lens = new Vector();
      int version = 0;

      synchronized (cookie_list)
      {
         Enumeration list = cookie_list.elements();
         Vector remove_list = null;

         while (list.hasMoreElements())
         {
            Cookie cookie = (Cookie)list.nextElement();

            if (cookie.hasExpired())
            {
               if (LOG.isDebugEnabled())
                  LOG.debug("Cookie has expired and is being removed: " + cookie);

               if (remove_list == null)
                  remove_list = new Vector();
               remove_list.addElement(cookie);
               continue;
            }

            if (cookie.sendWith(req) && (cookie_handler == null || cookie_handler.sendCookie(cookie, req)))
            {
               int len = cookie.getPath().length();
               int idx;

               // insert in correct position
               for (idx = 0; idx < lens.size(); idx++)
                  if (((Integer)lens.elementAt(idx)).intValue() < len)
                     break;

               names.insertElementAt(cookie.toExternalForm(), idx);
               lens.insertElementAt(new Integer(len), idx);

               if (cookie instanceof Cookie2)
                  version = Math.max(version, ((Cookie2)cookie).getVersion());
            }
         }

         // remove any marked cookies
         // Note: we can't do this during the enumeration!
         if (remove_list != null)
         {
            for (int idx = 0; idx < remove_list.size(); idx++)
               cookie_list.remove(remove_list.elementAt(idx));
         }
      }

      if (!names.isEmpty())
      {
         StringBuffer value = new StringBuffer();

         if (version > 0)
            value.append("$Version=\"" + version + "\"; ");

         value.append((String)names.elementAt(0));
         for (int idx = 1; idx < names.size(); idx++)
         {
            value.append("; ");
            value.append((String)names.elementAt(idx));
         }
         hdrs = Util.resizeArray(hdrs, hdrs.length + 1);
         hdrs[hdrs.length - 1] = new NVPair("Cookie", value.toString());

         // add Cookie2 header if necessary
         if (version != 1) // we currently know about version 1 only
         {
            int idx;
            for (idx = 0; idx < hdrs.length; idx++)
               if (hdrs[idx].getName().equalsIgnoreCase("Cookie2"))
                  break;
            if (idx == hdrs.length)
            {
               hdrs = Util.resizeArray(hdrs, hdrs.length + 1);
               hdrs[hdrs.length - 1] = new NVPair("Cookie2", "$Version=\"1\"");
            }
         }

         req.setHeaders(hdrs);

         if (LOG.isDebugEnabled())
            LOG.debug("Sending cookies '" + value + "'");

      }

      return REQ_CONTINUE;
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void responsePhase1Handler(Response resp, RoRequest req) throws IOException
   {
      String set_cookie = resp.getHeader("Set-Cookie");
      String set_cookie2 = resp.getHeader("Set-Cookie2");
      if (set_cookie == null && set_cookie2 == null)
         return;

      resp.deleteHeader("Set-Cookie");
      resp.deleteHeader("Set-Cookie2");

      if (set_cookie != null)
         handleCookie(set_cookie, false, req, resp);
      if (set_cookie2 != null)
         handleCookie(set_cookie2, true, req, resp);
   }

   /**
    * Invoked by the HTTPClient.
    */
   public int responsePhase2Handler(Response resp, Request req)
   {
      return RSP_CONTINUE;
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void responsePhase3Handler(Response resp, RoRequest req)
   {
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void trailerHandler(Response resp, RoRequest req) throws IOException
   {
      String set_cookie = resp.getTrailer("Set-Cookie");
      String set_cookie2 = resp.getTrailer("Set-Cookie2");
      if (set_cookie == null && set_cookie2 == null)
         return;

      resp.deleteTrailer("Set-Cookie");
      resp.deleteTrailer("Set-Cookie2");

      if (set_cookie != null)
         handleCookie(set_cookie, false, req, resp);
      if (set_cookie2 != null)
         handleCookie(set_cookie2, true, req, resp);
   }

   private void handleCookie(String set_cookie, boolean cookie2, RoRequest req, Response resp) throws ProtocolException
   {
      Cookie[] cookies;
      if (cookie2)
         cookies = Cookie2.parse(set_cookie, req);
      else
         cookies = Cookie.parse(set_cookie, req);

      if (LOG.isDebugEnabled())
      {
         LOG.debug("Received and parsed " + cookies.length + " cookies:");
         for (int idx = 0; idx < cookies.length; idx++)
            LOG.debug("Cookie " + idx + ": " + cookies[idx]);
      }

      Hashtable cookie_list = Util.getList(cookie_cntxt_list, req.getConnection().getContext());
      synchronized (cookie_list)
      {
         for (int idx = 0; idx < cookies.length; idx++)
         {
            Cookie cookie = (Cookie)cookie_list.get(cookies[idx]);
            if (cookie != null && cookies[idx].hasExpired())
            {
               if (LOG.isDebugEnabled())
                  LOG.debug("Cookie has expired and is " + "being removed: " + cookie);

               cookie_list.remove(cookie); // expired, so remove
            }
            else if (!cookies[idx].hasExpired()) // new or replaced
            {
               if (cookie_handler == null || cookie_handler.acceptCookie(cookies[idx], req, resp))
                  cookie_list.put(cookies[idx], cookies[idx]);
            }
         }
      }
   }

   /**
    * Discard all cookies for all contexts. Cookies stored in persistent storage
    * are not affected.
    */
   public static void discardAllCookies()
   {
      cookie_cntxt_list.clear();
   }

   /**
    * Discard all cookies for the given context. Cookies stored in persistent
    * storage are not affected.
    * @param context the context Object
    */
   public static void discardAllCookies(Object context)
   {
      if (context != null)
         cookie_cntxt_list.remove(context);
   }

   /**
    * List all stored cookies for all contexts.
    * @return an array of all Cookies
    * @since V0.3-1
    */
   public static Cookie[] listAllCookies()
   {
      synchronized (cookie_cntxt_list)
      {
         Cookie[] cookies = new Cookie[0];
         int idx = 0;

         Enumeration cntxt_list = cookie_cntxt_list.elements();
         while (cntxt_list.hasMoreElements())
         {
            Hashtable cntxt = (Hashtable)cntxt_list.nextElement();
            synchronized (cntxt)
            {
               cookies = Util.resizeArray(cookies, idx + cntxt.size());
               Enumeration cookie_list = cntxt.elements();
               while (cookie_list.hasMoreElements())
                  cookies[idx++] = (Cookie)cookie_list.nextElement();
            }
         }

         return cookies;
      }
   }

   /**
    * List all stored cookies for a given context.
    * @param context the context Object.
    * @return an array of Cookies
    * @since V0.3-1
    */
   public static Cookie[] listAllCookies(Object context)
   {
      Hashtable cookie_list = Util.getList(cookie_cntxt_list, context);

      synchronized (cookie_list)
      {
         Cookie[] cookies = new Cookie[cookie_list.size()];
         int idx = 0;

         Enumeration en = cookie_list.elements();
         while (en.hasMoreElements())
            cookies[idx++] = (Cookie)en.nextElement();

         return cookies;
      }
   }

   /**
    * Add the specified cookie to the list of cookies in the default context. If
    * a compatible cookie (as defined by <var>Cookie.equals()</var>) already
    * exists in the list then it is replaced with the new cookie.
    * @param cookie the Cookie to add
    * @since V0.3-1
    */
   public static void addCookie(Cookie cookie)
   {
      Hashtable cookie_list = Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext());
      cookie_list.put(cookie, cookie);
   }

   /**
    * Add the specified cookie to the list of cookies for the specified context.
    * If a compatible cookie (as defined by <var>Cookie.equals()</var>) already
    * exists in the list then it is replaced with the new cookie.
    * @param cookie the cookie to add
    * @param context the context Object.
    * @since V0.3-1
    */
   public static void addCookie(Cookie cookie, Object context)
   {
      Hashtable cookie_list = Util.getList(cookie_cntxt_list, context);
      cookie_list.put(cookie, cookie);
   }

   /**
    * Remove the specified cookie from the list of cookies in the default
    * context. If the cookie is not found in the list then this method does
    * nothing.
    * @param cookie the Cookie to remove
    * @since V0.3-1
    */
   public static void removeCookie(Cookie cookie)
   {
      Hashtable cookie_list = Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext());
      cookie_list.remove(cookie);
   }

   /**
    * Remove the specified cookie from the list of cookies for the specified
    * context. If the cookie is not found in the list then this method does
    * nothing.
    * @param cookie the cookie to remove
    * @param context the context Object
    * @since V0.3-1
    */
   public static void removeCookie(Cookie cookie, Object context)
   {
      Hashtable cookie_list = Util.getList(cookie_cntxt_list, context);
      cookie_list.remove(cookie);
   }

   /**
    * Sets a new cookie policy handler. This handler will be called for each
    * cookie that a server wishes to set and for each cookie that this module
    * wishes to send with a request. In either case the handler may allow or
    * reject the operation. If you wish to blindly accept and send all cookies
    * then just disable the handler with
    * <code>CookieModule.setCookiePolicyHandler(null);</code>.
    * <P>
    * At initialization time a default handler is installed. This handler allows
    * all cookies to be sent. For any cookie that a server wishes to be set two
    * lists are consulted. If the server matches any host or domain in the reject
    * list then the cookie is rejected; if the server matches any host or domain
    * in the accept list then the cookie is accepted (in that order). If no host
    * or domain match is found in either of these two lists and user interaction
    * is allowed then a dialog box is poped up to ask the user whether to accept
    * or reject the cookie; if user interaction is not allowed the cookie is
    * accepted.
    * <P>
    * The accept and reject lists in the default handler are initialized at
    * startup from the two properties <var>HTTPClient.cookies.hosts.accept</var>
    * and <var>HTTPClient.cookies.hosts.reject</var>. These properties must
    * contain a "|" separated list of host and domain names. All names beginning
    * with a "." are treated as domain names, all others as host names. An empty
    * string will match all hosts. The two lists are further expanded if the user
    * chooses one of the "Accept All from Domain" or "Reject All from Domain"
    * buttons in the dialog box.
    * <P>
    * Note: the default handler does not implement the rules concerning
    * unverifiable transactions (section 3.3.6, <A
    * HREF="http://www.ietf.org/rfc/rfc2965.txt">RFC-2965</A>). The reason for
    * this is simple: the default handler knows nothing about the application
    * using this client, and it therefore does not have enough information to
    * determine when a request is verifiable and when not. You are therefore
    * encouraged to provide your own handler which implements section 3.3.6 (use
    * the <code>CookiePolicyHandler.sendCookie</code> method for this).
    * @param handler the new policy handler
    * @return the previous policy handler
    */
   public static synchronized CookiePolicyHandler setCookiePolicyHandler(CookiePolicyHandler handler)
   {
      CookiePolicyHandler old = cookie_handler;
      cookie_handler = handler;
      return old;
   }
}

/**
 * A simple cookie policy handler.
 */
class DefaultCookiePolicyHandler implements CookiePolicyHandler
{
   /** a list of all hosts and domains from which to silently accept cookies */
   private String[] accept_domains = new String[0];

   /** a list of all hosts and domains from which to silently reject cookies */
   private String[] reject_domains = new String[0];

   /** the query popup */
   private BasicCookieBox popup = null;

   DefaultCookiePolicyHandler()
   {
      // have all cookies been accepted or rejected?
      String list;

      try
      {
         list = System.getProperty("HTTPClient.cookies.hosts.accept");
      }
      catch (Exception e)
      {
         list = null;
      }
      String[] domains = Util.splitProperty(list);
      for (int idx = 0; idx < domains.length; idx++)
         addAcceptDomain(domains[idx].toLowerCase());

      try
      {
         list = System.getProperty("HTTPClient.cookies.hosts.reject");
      }
      catch (Exception e)
      {
         list = null;
      }
      domains = Util.splitProperty(list);
      for (int idx = 0; idx < domains.length; idx++)
         addRejectDomain(domains[idx].toLowerCase());
   }

   /**
    * returns whether this cookie should be accepted. First checks the stored
    * lists of accept and reject domains, and if it is neither accepted nor
    * rejected by these then query the user via a popup.
    * @param cookie the cookie in question
    * @param req the request
    * @param resp the response
    * @return true if we accept this cookie.
    */
   public boolean acceptCookie(Cookie cookie, RoRequest req, RoResponse resp)
   {
      String server = req.getConnection().getHost();

      if (server.indexOf('.') == -1)
      {
         server += ".local"; //NOSONAR
      }

      // Check lists. Reject takes priority over accept

      for (int idx = 0; idx < reject_domains.length; idx++)
      {
         if (reject_domains[idx].length() == 0 || reject_domains[idx].charAt(0) == '.'
            && server.endsWith(reject_domains[idx]) || reject_domains[idx].charAt(0) != '.'
            && server.equals(reject_domains[idx]))
            return false;
      }

      for (int idx = 0; idx < accept_domains.length; idx++)
      {
         if (accept_domains[idx].length() == 0 || accept_domains[idx].charAt(0) == '.'
            && server.endsWith(accept_domains[idx]) || accept_domains[idx].charAt(0) != '.'
            && server.equals(accept_domains[idx]))
            return true;
      }

      // Ok, not in any list, so ask the user (if allowed).

      if (!req.allowUI())
         return true;

      if (popup == null)
         popup = new BasicCookieBox();

      return popup.accept(cookie, this, server);
   }

   /**
    * This handler just allows all cookies to be sent which were accepted (i.e.
    * no further restrictions are placed on the sending of cookies).
    * @return true
    */
   public boolean sendCookie(Cookie cookie, RoRequest req)
   {
      return true;
   }

   void addAcceptDomain(String domain)
   {
      if (domain.indexOf('.') == -1 && domain.length() > 0)
         domain += ".local";

      for (int idx = 0; idx < accept_domains.length; idx++)
      {
         if (domain.endsWith(accept_domains[idx]))
            return;
         if (accept_domains[idx].endsWith(domain))
         {
            accept_domains[idx] = domain;
            return;
         }
      }
      accept_domains = Util.resizeArray(accept_domains, accept_domains.length + 1);
      accept_domains[accept_domains.length - 1] = domain;
   }

   void addRejectDomain(String domain)
   {
      if (domain.indexOf('.') == -1 && domain.length() > 0)
         domain += ".local";

      for (int idx = 0; idx < reject_domains.length; idx++)
      {
         if (domain.endsWith(reject_domains[idx]))
            return;
         if (reject_domains[idx].endsWith(domain))
         {
            reject_domains[idx] = domain;
            return;
         }
      }

      reject_domains = Util.resizeArray(reject_domains, reject_domains.length + 1);
      reject_domains[reject_domains.length - 1] = domain;
   }
}

/**
 * A simple popup that asks whether the cookie should be accepted or rejected,
 * or if cookies from whole domains should be silently accepted or rejected.
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal���r
 */
class BasicCookieBox extends Frame
{
   private final static String title = "Set Cookie Request";

   private Dimension screen;

   private GridBagConstraints constr;

   private Label name_value_label;

   private Label domain_value;

   private Label ports_label;

   private Label ports_value;

   private Label path_value;

   private Label expires_value;

   private Label discard_note;

   private Label secure_note;

   private Label c_url_note;

   private Panel left_panel;

   private Panel right_panel;

   private Label comment_label;

   private TextArea comment_value;

   private TextField domain;

   private Button default_focus;

   private boolean accept;

   private boolean accept_domain;

   /**
    * Constructs the popup.
    */
   BasicCookieBox()
   {
      super(title);

      screen = getToolkit().getScreenSize();

      addNotify();
      addWindowListener(new Close());

      GridBagLayout layout;
      setLayout(layout = new GridBagLayout());
      constr = new GridBagConstraints();

      constr.gridwidth = GridBagConstraints.REMAINDER;
      constr.anchor = GridBagConstraints.WEST;
      add(new Label("The server would like to set the following cookie:"), constr);

      Panel p = new Panel();
      left_panel = new Panel();
      left_panel.setLayout(new GridLayout(4, 1));
      left_panel.add(new Label("Name=Value:"));
      left_panel.add(new Label("Domain:"));
      left_panel.add(new Label("Path:"));
      left_panel.add(new Label("Expires:"));
      ports_label = new Label("Ports:");
      p.add(left_panel);

      right_panel = new Panel();
      right_panel.setLayout(new GridLayout(4, 1));
      right_panel.add(name_value_label = new Label());
      right_panel.add(domain_value = new Label());
      right_panel.add(path_value = new Label());
      right_panel.add(expires_value = new Label());
      ports_value = new Label();
      p.add(right_panel);
      add(p, constr);
      secure_note = new Label("This cookie will only be sent over secure connections");
      discard_note = new Label("This cookie will be discarded at the end of the session");
      c_url_note = new Label("");
      comment_label = new Label("Comment:");
      comment_value = new TextArea("", 3, 45, TextArea.SCROLLBARS_VERTICAL_ONLY);
      comment_value.setEditable(false);

      add(new Panel(), constr);

      constr.gridwidth = 1;
      constr.anchor = GridBagConstraints.CENTER;
      constr.weightx = 1.0;
      add(default_focus = new Button("Accept"), constr);
      default_focus.addActionListener(new Accept());

      Button b;
      constr.gridwidth = GridBagConstraints.REMAINDER;
      add(b = new Button("Reject"), constr);
      b.addActionListener(new Reject());

      constr.weightx = 0.0;
      p = new Separator();
      constr.fill = GridBagConstraints.HORIZONTAL;
      add(p, constr);

      constr.fill = GridBagConstraints.NONE;
      constr.anchor = GridBagConstraints.WEST;
      add(new Label("Accept/Reject all cookies from a host or domain:"), constr);

      p = new Panel();
      p.add(new Label("Host/Domain:"));
      p.add(domain = new TextField(30));
      add(p, constr);

      add(new Label("domains are characterized by a leading dot (`.');"), constr);
      add(new Label("an empty string matches all hosts"), constr);

      constr.anchor = GridBagConstraints.CENTER;
      constr.gridwidth = 1;
      constr.weightx = 1.0;
      add(b = new Button("Accept All"), constr);
      b.addActionListener(new AcceptDomain());

      constr.gridwidth = GridBagConstraints.REMAINDER;
      add(b = new Button("Reject All"), constr);
      b.addActionListener(new RejectDomain());

      pack();

      constr.anchor = GridBagConstraints.WEST;
      constr.gridwidth = GridBagConstraints.REMAINDER;
   }

   @Override
   public Dimension getMaximumSize()
   {
      return new Dimension(screen.width * 3 / 4, screen.height * 3 / 4);
   }

   /**
    * our event handlers
    */
   private class Accept implements ActionListener
   {
      public void actionPerformed(ActionEvent ae)
      {
         accept = true;
         accept_domain = false;
         synchronized (BasicCookieBox.this)
         {
            BasicCookieBox.this.notifyAll();
         }
      }
   }

   private class Reject implements ActionListener
   {
      public void actionPerformed(ActionEvent ae)
      {
         accept = false;
         accept_domain = false;
         synchronized (BasicCookieBox.this)
         {
            BasicCookieBox.this.notifyAll();
         }
      }
   }

   private class AcceptDomain implements ActionListener
   {
      public void actionPerformed(ActionEvent ae)
      {
         accept = true;
         accept_domain = true;
         synchronized (BasicCookieBox.this)
         {
            BasicCookieBox.this.notifyAll();
         }
      }
   }

   private class RejectDomain implements ActionListener
   {
      public void actionPerformed(ActionEvent ae)
      {
         accept = false;
         accept_domain = true;
         synchronized (BasicCookieBox.this)
         {
            BasicCookieBox.this.notifyAll();
         }
      }
   }

   private class Close extends WindowAdapter
   {
      @Override
      public void windowClosing(WindowEvent we)
      {
         new Reject().actionPerformed(null);
      }
   }

   /**
    * the method called by the DefaultCookiePolicyHandler.
    * @return true if the cookie should be accepted
    */
   public synchronized boolean accept(Cookie cookie, DefaultCookiePolicyHandler h, String server)
   {
      // set the new values

      name_value_label.setText(cookie.getName() + "=" + cookie.getValue());
      domain_value.setText(cookie.getDomain());
      path_value.setText(cookie.getPath());
      if (cookie.expires() == null)
         expires_value.setText("never");
      else
         expires_value.setText(cookie.expires().toString());
      int pos = 2;
      if (cookie.isSecure())
         add(secure_note, constr, pos++);
      if (cookie.discard())
         add(discard_note, constr, pos++);

      if (cookie instanceof Cookie2)
      {
         Cookie2 cookie2 = (Cookie2)cookie;

         // set ports list
         if (cookie2.getPorts() != null)
         {
            ((GridLayout)left_panel.getLayout()).setRows(5);
            left_panel.add(ports_label, 2);
            ((GridLayout)right_panel.getLayout()).setRows(5);
            int[] ports = cookie2.getPorts();
            StringBuffer plist = new StringBuffer();
            plist.append(ports[0]);
            for (int idx = 1; idx < ports.length; idx++)
            {
               plist.append(", ");
               plist.append(ports[idx]);
            }
            ports_value.setText(plist.toString());
            right_panel.add(ports_value, 2);
         }

         // set comment url
         if (cookie2.getCommentURL() != null)
         {
            c_url_note.setText("For more info on this cookie see: " + cookie2.getCommentURL());
            add(c_url_note, constr, pos++);
         }

         // set comment
         if (cookie2.getComment() != null)
         {
            comment_value.setText(cookie2.getComment());
            add(comment_label, constr, pos++);
            add(comment_value, constr, pos++);
         }
      }

      // invalidate all labels, so that new values are displayed correctly

      name_value_label.invalidate();
      domain_value.invalidate();
      ports_value.invalidate();
      path_value.invalidate();
      expires_value.invalidate();
      left_panel.invalidate();
      right_panel.invalidate();
      secure_note.invalidate();
      discard_note.invalidate();
      c_url_note.invalidate();
      comment_value.invalidate();
      invalidate();

      // set default domain test

      domain.setText(cookie.getDomain());

      // display

      setResizable(true);
      pack();
      setResizable(false);
      setLocation((screen.width - getPreferredSize().width) / 2,
         (int)((screen.height - getPreferredSize().height) / 2 * .7));
      setVisible(true);
      default_focus.requestFocus();

      // wait for user input

      try
      {
         wait();
      }
      catch (InterruptedException e)
      {
      }

      setVisible(false);

      // reset popup

      remove(secure_note);
      remove(discard_note);
      left_panel.remove(ports_label);
      ((GridLayout)left_panel.getLayout()).setRows(4);
      right_panel.remove(ports_value);
      ((GridLayout)right_panel.getLayout()).setRows(4);
      remove(c_url_note);
      remove(comment_label);
      remove(comment_value);

      // handle accept/reject domain buttons

      if (accept_domain)
      {
         String dom = domain.getText().trim().toLowerCase();

         if (accept)
            h.addAcceptDomain(dom);
         else
            h.addRejectDomain(dom);
      }

      return accept;
   }
}

/**
 * A simple separator element.
 */
class Separator extends Panel
{
   @Override
   public void paint(Graphics g)
   {
      int w = getSize().width, h = getSize().height / 2;

      g.setColor(Color.darkGray);
      g.drawLine(2, h - 1, w - 2, h - 1);
      g.setColor(Color.white);
      g.drawLine(2, h, w - 2, h);
   }

   @Override
   public Dimension getMinimumSize()
   {
      return new Dimension(4, 2);
   }
}
