// Serve - minimal Java servlet container class
//
// Copyright (C)1996,1998 by Jef Poskanzer <jef@acme.com>. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// Visit the ACME Labs Java page for up-to-date versions of this and other
// fine Java utilities: http://www.acme.com/java/
//

// All enhancements Copyright (C)1998-2010 by Dmitriy Rogatkin
// This version is compatible with JSDK 2.5
// http://tjws.sourceforge.net
// $Id: Serve.java,v 1.194 2009/12/31 05:02:13 dmitriy Exp $

package Acme.Serve;

import Acme.Utils;

import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.SingleThreadModel;
import javax.servlet.UnavailableException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/// Minimal Java servlet container class.
// <P>
// This class implements a very small embeddable servlet container.
// It runs Servlets compatible with the API used by Sun's 
// <A HREF="http://docs.sun.com/app/docs/doc/819-3653">Java System Application </A> server.
// Servlet API can be found <A HREF="http://java.sun.com/products/servlet/">here</A>.
// It comes with default Servlets which provide the usual
// httpd services, returning files and directory listings.
// <P>
// This is not in any sense a competitor for Java System Application server.
// Java System Application server is a full-fledged HTTP server and more.
// Acme.Serve is tiny, about 5000 lines, and provides only the
// functionality necessary to deliver an Applet's .class files
// and then start up a Servlet talking to the Applet.
// They are both written in Java, they are both web servers, and
// they both implement the Servlet API; other than that they couldn't
// be more different.
// <P>
// This is actually the second HTTP server I've written.
// The other one is called
// <A HREF="http://www.acme.com/software/thttpd/">thttpd</A>,
// it's written in C, and is also pretty small although much more
// featureful than this.
// <P>
// Other Java HTTP servers:
// <UL>
// <LI> The above-mentioned <A
// HREF="http://docs.sun.com/app/docs/doc/819-3653">JavaServer</A>.
// <LI> W3C's <A HREF="http://www.w3.org/pub/WWW/Jigsaw/">Jigsaw</A>.
// <LI> David Wilkinson's <A
// HREF="http://www.netlink.co.uk/users/cascade/http/">Cascade</A>.
// <LI> Yahoo's <A
// HREF="http://www.yahoo.com/Computers_and_Internet/Software/Internet/World_Wide_Web/Servers/Java/">list
// of Java web servers</A>.
// </UL>
// <P>
// A <A HREF="http://www.byte.com/art/9706/sec8/art1.htm">June 1997 BYTE
// magazine article</A> mentioning this server.<BR>
// A <A HREF="http://www.byte.com/art/9712/sec6/art7.htm">December 1997 BYTE
// magazine article</A> giving it an Editor's Choice Award of Distinction.<BR>
// <A HREF="/resources/classes/Acme/Serve/Serve.java">Fetch the
// software.</A><BR>
// <A HREF="/resources/classes/Acme.tar.Z">Fetch the entire Acme package.</A>
// <P>
// @see Acme.Serve.servlet.http.HttpServlet
// @see FileServlet
// @see CgiServlet
// <h3>Post notes</h3>
// Currently the server 3 more times complex and can compete with
// most popular app and web servers used for deploying of web
// Java applications.

// Inheritance can extend usage of this server
public class Serve implements ServletContext, Serializable
{

   public static final String ARG_PORT = "port";

   public static final String ARG_THROTTLES = "throttles";

   public static final String ARG_SERVLETS = "servlets";

   public static final String ARG_REALMS = "realms";

   public static final String ARG_ALIASES = "aliases";

   public static final String ARG_BINDADDRESS = "bind-address";

   public static final String ARG_BACKLOG = "backlog";

   public static final String ARG_CGI_PATH = "cgi-path";

   public static final String ARG_ERR = "error-stream";

   public static final String ARG_OUT = "out-stream";

   public static final String ARG_SESSION_TIMEOUT = "session-timeout";

   public static final String ARG_LOG_DIR = "log-dir";

   public static final String ARG_LOG_OPTIONS = "log-options";

   public static final String ARG_NOHUP = "nohup";

   public static final String ARG_JSP = "JSP";

   public static final String ARG_WAR = "war-deployer";

   public static final String ARG_KEEPALIVE = "keep-alive";

   public static final String DEF_LOGENCODING = "tjws.serve.log.encoding";

   public static final String ARG_KEEPALIVE_TIMEOUT = "timeout-keep-alive";

   public static final String ARG_MAX_CONN_USE = "max-alive-conn-use";

   public static final String ARG_SESSION_PERSIST = "sssn-persistance";

   public static final String ARG_MAX_ACTIVE_SESSIONS = "max-active-sessions";

   public static final String ARG_ACCESS_LOG_FMT = "access-log-format";

   public static final String ARG_ACCEPTOR_CLASS = "acceptorImpl";

   public static final String ARG_WORK_DIRECTORY = "workdirectory";

   public static final String ARG_SESSION_SEED = "SessionSeed";

   public static final String ARG_THREAD_POOL_SIZE = Utils.ThreadPool.MAXNOTHREAD;


   protected static final int DEF_SESSION_TIMEOUT = 30; // in minutes

   protected static final int DEF_MIN_ACT_SESS = 10;

   protected static final int DESTROY_TIME_SEC = 15;

   protected static final int HTTP_MAX_HDR_LEN = 1024 * 1024 * 10;

   public static final int DEF_PORT = 8080;

   public static final String BGCOLOR = "BGCOLOR=\"#D1E9FE\"";

   /**
    * max number of alive connections default value
    */
   protected static final int DEF_MAX_CONN_USE = 100;

   public static final String UTF8 = "UTF-8"; // default encoding

   protected String hostName;

   private transient PrintStream logStream;

   private boolean useAccLog;

   private boolean keepAlive;

   private int timeoutKeepAlive;

   private int maxAliveConnUse;

   private boolean showUserAgent;

   private boolean showReferer;

   protected String keepAliveHdrParams;

   protected transient PathTreeDictionary registry;

   protected transient PathTreeDictionary realms;

   protected transient PathTreeDictionary mappingtable;

   private Hashtable attributes;

   protected transient KeepAliveCleaner keepAliveCleaner;

   protected transient ThreadGroup serverThreads;

   protected transient Utils.ThreadPool threadPool;

   protected transient Constructor gzipInStreamConstr;

   // for sessions
   private byte[] uniqer = new byte[20]; // TODO consider configurable strength

   private SecureRandom srandom;

   protected HttpSessionContextImpl sessions;

   protected int expiredIn;

   public Map arguments;

   public Properties mime;

   protected List<ServeConnection> connections = new ArrayList<ServeConnection>();

   // / Constructor.
   public Serve(Map arguments, PrintStream logStream)
   {
      this.arguments = arguments;
      this.logStream = logStream;
      registry = new PathTreeDictionary();
      realms = new PathTreeDictionary();
      attributes = new Hashtable();
      serverThreads = new ThreadGroup("TJWS threads");
      Properties props = new Properties();
      props.putAll(arguments);
      // TODO do not create thread pool unless requested
      threadPool = new Utils.ThreadPool(props, new Utils.ThreadFactory()
      {
         public Thread create(Runnable runnable)
         {
            Thread result = new Thread(serverThreads, runnable);
            result.setDaemon(true);
            return result;
         }
      });
      setAccessLogged();
      keepAlive = arguments.get(ARG_KEEPALIVE) == null || ((Boolean) arguments.get(ARG_KEEPALIVE)).booleanValue();
      System.out.println("KEEPALIVE!: " + keepAlive);
      int timeoutKeepAliveSec;
      try
      {
         timeoutKeepAliveSec = Integer.parseInt((String) arguments.get(ARG_KEEPALIVE_TIMEOUT));
      }
      catch (Exception ex)
      {
         timeoutKeepAliveSec = 30;
      }
      timeoutKeepAlive = timeoutKeepAliveSec * 1000;
      try
      {
         maxAliveConnUse = Integer.parseInt((String) arguments.get(ARG_MAX_CONN_USE));
      }
      catch (Exception ex)
      {
         maxAliveConnUse = DEF_MAX_CONN_USE;
      }
      keepAliveHdrParams = "timeout=" + timeoutKeepAliveSec + ", max=" + maxAliveConnUse;

      expiredIn = arguments.get(ARG_SESSION_TIMEOUT) != null ? ((Integer) arguments.get(ARG_SESSION_TIMEOUT))
              .intValue() : DEF_SESSION_TIMEOUT;
      srandom = new SecureRandom((arguments.get(ARG_SESSION_SEED) == null ? "TJWS" + new Date() : (String) arguments.get(ARG_SESSION_SEED)).getBytes());
      try
      {
         gzipInStreamConstr = Class.forName("java.util.zip.GZIPInputStream").getConstructor(new Class[]{InputStream.class});
      }
      catch (ClassNotFoundException cne)
      {

      }
      catch (NoSuchMethodException nsm)
      {

      }
      initMime();
   }

   /**
    * Default constructor to create TJWS as a bean
    */
   public Serve()
   {
      this(new HashMap(), System.err);
   }

   protected void setAccessLogged()
   {
      String logflags = (String) arguments.get(ARG_LOG_OPTIONS);
      if (logflags != null)
      {
         useAccLog = true;
         showUserAgent = logflags.indexOf('A') >= 0;
         showReferer = logflags.indexOf('R') >= 0;
      }
   }

   protected boolean isAccessLogged()
   {
      return useAccLog;
   }

   protected boolean isShowReferer()
   {
      return showReferer;
   }

   protected boolean isShowUserAgent()
   {
      return showUserAgent;
   }

   protected boolean isKeepAlive()
   {
      return keepAlive;
   }

   protected int getKeepAliveDuration()
   {
      return timeoutKeepAlive;
   }

   protected String getKeepAliveParamStr()
   {
      return keepAliveHdrParams;
   }

   protected int getMaxTimesConnectionUse()
   {
      return maxAliveConnUse;
   }

   protected void initMime()
   {
      mime = new Properties();
      try
      {
         mime.load(getClass().getClassLoader().getResourceAsStream("Acme/Resource/mime.properties"));
      }
      catch (Exception ex)
      {
         log("MIME map can't be loaded:" + ex);
      }
   }

   // / Register a Servlet by class name. Registration consists of a URL
   // pattern, which can contain wildcards, and the class name of the Servlet
   // to launch when a matching URL comes in. Patterns are checked for
   // matches in the order they were added, and only the first match is run.
   public void addServlet(String urlPat, String className)
   {
      addServlet(urlPat, className, (Hashtable) null);
   }

   /**
    * Adds a servlet to run
    *
    * @param urlPat     servlet invoker URL pattern
    * @param className  servlet class name
    * @param initParams servlet init parameters
    */
   public void addServlet(String urlPat, String className, Hashtable initParams)
   {
      // Check if we're allowed to make one of these.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         int i = className.lastIndexOf('.');
         if (i > 0)
         {
            security.checkPackageAccess(className.substring(0, i));
            security.checkPackageDefinition(className.substring(0, i));
         }
      }

      // Make a new one.
      try
      {
         addServlet(urlPat, (Servlet) Class.forName(className).newInstance(), initParams);
      }
      catch (ClassNotFoundException e)
      {
         log("Class not found: " + className);
         ClassLoader cl = getClass().getClassLoader();
         log("Class loader: " + cl);
         if (cl instanceof java.net.URLClassLoader)
            log("CP: " + java.util.Arrays.asList(((java.net.URLClassLoader) cl).getURLs()));
      }
      catch (ClassCastException e)
      {
         log("Servlet class doesn't implement javax.servlet.Servlet: " + e.getMessage());
      }
      catch (InstantiationException e)
      {
         log("Can't instantiate servlet: " + e.getMessage());
      }
      catch (IllegalAccessException e)
      {
         log("Illegal class access: " + e.getMessage());
      }
      catch (Exception e)
      {
         log("Unexpected problem of servlet creation: " + e, e);
      }
   }

   /**
    * Register a Servlet. Registration consists of a URL pattern,
    * which can contain wildcards, and the Servlet to
    * launch when a matching URL comes in. Patterns are checked for
    * matches in the order they were added, and only the first match is run.
    *
    * @param urlPat  servlet invoker URL pattern
    * @param servlet already instantiated servlet but init
    */
   public void addServlet(String urlPat, Servlet servlet)
   {
      addServlet(urlPat, servlet, (Hashtable) null);
   }

   /**
    * Register a Servlet
    *
    * @param urlPat
    * @param servlet
    * @param initParams
    */
   public synchronized void addServlet(String urlPat, Servlet servlet, Hashtable initParams)
   {
      try
      {
         if (getServlet(urlPat) != null)
            log("Servlet overriden by " + servlet + ", for path:" + urlPat);
         servlet.init(new ServeConfig((ServletContext) this, initParams, urlPat));
         registry.put(urlPat, servlet);
      }
      catch (ServletException e)
      { //
         // it handles UnavailableException as well without an attempt to re-adding
         log("Problem initializing servlet, it won't be used: " + e);
      }
   }

   public Servlet unloadServlet(Servlet servlet)
   {
      Servlet result = null;
      synchronized (registry)
      {
         result = (Servlet) registry.remove(servlet)[0];
      }
      return result;
   }

   public synchronized void unloadServlet(String urlPat)
   {
      Servlet servlet = (Servlet) registry.remove(urlPat)[0];
      if (servlet != null)
         servlet.destroy(); // sessions associated with it have to be invalidated to free up any the servlet specific object
      // TODO decide if UnavailableException should be thrown at access
   }

   // / Register a standard set of Servlets. These will return
   // files or directory listings, and run CGI programs, much like a
   // standard HTTP server.
   // <P>
   // Because of the pattern checking order, this should be called
   // <B>after</B> you've added any custom Servlets.
   // <P>
   // The current set of default servlet mappings:
   // <UL>
   // <LI> If enabled, *.cgi goes to CgiServlet, and gets run as a CGI program.
   // <LI> * goes to FileServlet, and gets served up as a file or directory.
   // </UL>
   // @param cgi whether to run CGI programs
   // TODO: provide user specified CGI directory
   public void addDefaultServlets(String cgi)
   {
      try
      {
         addDefaultServlets(cgi, null);
      }
      catch (IOException ioe)
      { /* ignore, makes sense only for throtles */
      }
   }

   /**
    * Register a standard set of Servlets, with optional throttles. These will return files or directory listings, and run CGI programs, much like a standard
    * HTTP server.
    * <p/>
    * Because of the pattern checking order, this should be called <B>after</B> you've added any custom Servlets.
    * <p/>
    * The current set of default servlet mappings:
    * <UL>
    * <LI> If enabled, *.cgi goes to CgiServlet, and gets run as a CGI program.
    * <LI> * goes to FileServlet, and gets served up as a file or directory.
    * </UL>
    *
    * @param cgi       whether to run CGI programs
    * @param throttles filename to read FileServlet throttle settings from, can be null
    * @throws IOException
    */
   public void addDefaultServlets(String cgi, String throttles) throws IOException
   {
      // TODO: provide user specified CGI directory
      if (cgi != null)
      {
         if (getServlet("/" + cgi) == null)
            addServlet("/" + cgi, new Acme.Serve.CgiServlet());
         else
            log("Servlet for path '/" + cgi + "' already defined and no default will be used.");
      }
      if (getServlet("/") == null)
         if (throttles != null)
            addServlet("/", new Acme.Serve.FileServlet(throttles, null));
         else
            addServlet("/", new Acme.Serve.FileServlet());
      else
         log("Servlet for path '/' already defined and no default will be used.");
   }

   protected void addWarDeployer(String deployerFactory, String throttles)
   {
      if (deployerFactory == null) // try to use def
         deployerFactory = "rogatkin.web.WarRoller";
      try
      {
         WarDeployer wd = (WarDeployer) Class.forName(deployerFactory).newInstance();
         wd.deploy(this);
      }
      catch (ClassNotFoundException cnf)
      {
         log("Problem initializing war deployer: " + cnf);
      }
      catch (Exception e)
      {
         log("Problem war(s) deployment", e);
      }
   }

   protected File getPersistentFile()
   {
      if (arguments.get(ARG_SESSION_PERSIST) == null || (Boolean) arguments.get(ARG_SESSION_PERSIST) == Boolean.FALSE)
         return null;
      String workPath = (String) arguments.get(ARG_WORK_DIRECTORY);
      if (workPath == null)
         workPath = ".";
      return new File(workPath, hostName + '-'
              + (arguments.get(ARG_PORT) == null ? String.valueOf(DEF_PORT) : arguments.get(ARG_PORT))
              + "-session.obj");
   }

   // Run the server. Returns only on errors.
   transient boolean running = true;

   protected transient Acceptor acceptor;

   protected transient Thread ssclThread;

   protected transient boolean initialized;

   protected CountDownLatch shutdownLatch;
   protected Thread backgroundThread;

   public void runInBackground()
   {
      shutdownLatch = new CountDownLatch(1);
      try
      {
         init();
      }
      catch (IOException e)
      {
         throw new RuntimeException(e);
      }
      backgroundThread = new Thread()
      {
         public void run()
         {
            try
            {
               serve();
            }
            finally
            {
               shutdownLatch.countDown();
            }
         }
      };
      backgroundThread.start();

   }

   public void stopBackground()
   {
      try
      {
         notifyStop();
      }
      catch (Throwable ignored)
      {
      }
      try
      {
         if (shutdownLatch.await(100, TimeUnit.MILLISECONDS) == false)
         {
            try
            {
               backgroundThread.interrupt();
            }
            catch (Exception ignored)
            {
            }
         }
         shutdownLatch.await(1, TimeUnit.SECONDS);
      }
      catch (InterruptedException e)
      {
      }
      synchronized(connections)
      {
         for (ServeConnection conn : connections)
         {
            conn.closeSocket();
         }
         connections.clear();
      }
   }

   /**
    * Launches the server
    * It doesn't exist until server runs, so start it in a dedicated thread.
    *
    * @return 0 if the server successfully terminated, 1 if it can't be started and -1 if it
    *         was terminated during some errors
    */
   public int serve()
   {
      try
      {
         while (running)
         {
            try
            {
               Socket socket = acceptor.accept();
               // TODO consider to use ServeConnection object pool
               if (keepAliveCleaner != null) // we need to add regardless of keep alive
                  keepAliveCleaner.addConnection(new ServeConnection(socket, this));
               else
                  new ServeConnection(socket, this); // TODO consider req/resp objects pooling
            }
            catch (IOException e)
            {
               log("Accept: " + e);
            }
            catch (SecurityException se)
            {
               log("Illegal access: " + se);
            }
            catch (IllegalStateException is)
            {
               log("Illegal state: " + is);
            }
         }
      }
      catch (Throwable t)
      {
         log("Unhandled exception: " + t + ", server is terminating.", t);
         if (t instanceof ThreadDeath)
            throw (Error) t;
         return -1;
      }
      finally
      {
         try
         {
            if (acceptor != null)
               acceptor.destroy();
         }
         catch (IOException e)
         {
         }
      }
      return 0;
   }

   public void init() throws IOException
   {
      shutdownLatch = new CountDownLatch(1);
      acceptor = createAcceptor();

      if (expiredIn > 0)
      {
         ssclThread = new Thread(serverThreads, new Runnable()
         {
            public void run()
            {
               while (running)
               {
                  try
                  {
                     Thread.sleep(expiredIn * 60 * 1000);
                  }
                  catch (InterruptedException ie)
                  {
                     if (running == false)
                        break;
                  }
                  Enumeration e = sessions.keys();
                  while (e.hasMoreElements())
                  {
                     Object sid = e.nextElement();
                     if (sid != null)
                     {
                        AcmeSession as = (AcmeSession) sessions.get(sid);
                        if (as != null && (as.checkExpired() || !as.isValid()))
                        { // log("sesion
                           as = (AcmeSession) sessions.remove(sid);
                           if (as != null && as.isValid())
                              try
                              {
                                 as.invalidate();
                              }
                              catch (IllegalStateException ise)
                              {

                              }
                        }
                     }
                  }
               }
            }
         }, "Session cleaner");
         ssclThread.setPriority(Thread.MIN_PRIORITY);
         // ssclThread.setDaemon(true);
         ssclThread.start();
      } // else
      // expiredIn = -expiredIn;
      if (isKeepAlive())
      {
         keepAliveCleaner = new KeepAliveCleaner();
         keepAliveCleaner.start();
      }
      File fsessions = getPersistentFile();
      if (fsessions != null && fsessions.exists())
      {
         BufferedReader br = null;
         try
         {
            br = new BufferedReader(new FileReader(fsessions));
            sessions = HttpSessionContextImpl.restore(br, Math.abs(expiredIn) * 60, this);
         }
         catch (IOException ioe)
         {
            log("Problem in restoring sessions.", ioe);
         }
         catch (Exception e)
         {
            log("Unexpected problem in restoring sessions.", e);
         }
         finally
         {
            if (br != null)
               try
               {
                  br.close();
               }
               catch (IOException ioe)
               {
               }
         }
      }
      if (sessions == null)
         sessions = new HttpSessionContextImpl();
      // TODO: display address as name and as ip
      System.out.println("[" + new Date() + "] TJWS httpd " + hostName + " - " + acceptor + " is listening.");
   }

   /**
    * Tells the server to stop
    *
    * @throws IOException
    */
   public void notifyStop() throws IOException
   {
      running = false;
      acceptor.destroy();
      acceptor = null;
      if (ssclThread != null)
         ssclThread.interrupt();
   }

   public static interface Acceptor
   {
      public void init(Map inProperties, Map outProperties) throws IOException;

      public Socket accept() throws IOException;

      public void destroy() throws IOException;
   }

   protected Acceptor createAcceptor() throws IOException
   {
      String acceptorClass = (String) arguments.get(ARG_ACCEPTOR_CLASS);
      if (acceptorClass == null)
         acceptorClass = "Acme.Serve.SimpleAcceptor";
      // assured defaulting here
      try
      {
         acceptor = (Acceptor) Class.forName(acceptorClass).newInstance();
      }
      catch (InstantiationException e)
      {
         log("Couldn't instantiate Acceptor, the Server is inoperable", e);
      }
      catch (IllegalAccessException e)
      {
         Constructor c;
         try
         {
            c = Class.forName(acceptorClass).getDeclaredConstructor(Utils.EMPTY_CLASSES);
            c.setAccessible(true);
            acceptor = (Acceptor) c.newInstance(Utils.EMPTY_OBJECTS);
         }
         catch (Exception e1)
         {
            log("Acceptor is not accessable or can't be instantiated, the Server is inoperable", e);
         }
      }
      catch (ClassNotFoundException e)
      {
         log("Acceptor class not found, the Server is inoperable", e);
      }
      Map acceptorProperties = new Properties();
      acceptor.init(arguments, acceptorProperties);
      hostName = (String) acceptorProperties.get(ARG_BINDADDRESS);
      return acceptor;
   }

   // Methods from ServletContext.

   // / Gets a servlet by name.
   // @param name the servlet name
   // @return null if the servlet does not exist

   public Servlet getServlet(String name)
   {
      try
      {
         return (Servlet) registry.get(name)[0];
      }
      catch (NullPointerException npe)
      {
         return null;
      }
   }

   // / Enumerates the servlets in this context (server). Only servlets that
   // are accesible will be returned. This enumeration always includes the
   // servlet itself.
   public Enumeration getServlets()
   {
      return registry.elements();
   }

   // / Enumerates the names of the servlets in this context (server). Only
   // servlets that are accesible will be returned. This enumeration always
   // includes the servlet itself.
   public Enumeration getServletNames()
   {
      return registry.keys();
   }

   // / Destroys all currently-loaded servlets.
   public synchronized void destroyAllServlets()
   {
      //log("Entering destroyAllServlets()", new Exception("Entering destroyAllServlets()"));
      // serialize sessions

      // invalidate all sessions
      // TODO consider merging two pieces below, generally if session is stored,
      // it shouldn't be invalidated
      File sf = getPersistentFile();
      if (sf != null && sessions != null)
      {
         Writer w = null;
         try
         {
            w = new FileWriter(sf);
            sessions.save(w);
            log("Sessions stored.");
         }
         catch (IOException ioe)
         {
            log("IO problem in storing sessions " + ioe);
         }
         catch (Throwable t)
         {
            log("Problem in storing sessions " + t);
         }
         finally
         {
            try
            {
               w.close();
            }
            catch (Exception e)
            {
            }
         }

         Enumeration e = sessions.keys();
         while (e.hasMoreElements())
         {
            Object sid = e.nextElement();
            if (sid != null)
            {
               AcmeSession as = (AcmeSession) sessions.get(sid);
               if (as != null)
               {
                  as = (AcmeSession) sessions.remove(sid);
                  if (as != null && as.isValid())
                     try
                     {
                        as.invalidate();
                     }
                     catch (IllegalStateException ise)
                     {

                     }
               }
            }
         }
      }
      // destroy servlets
      final Enumeration en = registry.elements();
      Runnable servletDestroyer = new Runnable()
      {
         public void run()
         {
            ((Servlet) en.nextElement()).destroy();
         }
      };
      int dhc = 0;
      while (en.hasMoreElements())
      {
         Thread destroyThread = new Thread(servletDestroyer, "Destroy");
         destroyThread.setDaemon(true);
         destroyThread.start();
         try
         {
            destroyThread.join(DESTROY_TIME_SEC * 1000);
         }
         catch (InterruptedException e)
         {
         }
         if (destroyThread.isAlive())
         {
            log("Destroy thread didn't terminate in " + DESTROY_TIME_SEC);
            destroyThread.setName("Destroy too long " + (dhc++)); // let it running with different name
            //destroyThread.stop();
         }
      }
      // clean access tree
      registry = new PathTreeDictionary();
   }

   protected void setMappingTable(PathTreeDictionary mappingtable)
   {
      this.mappingtable = mappingtable;
   }

   protected void setRealms(PathTreeDictionary realms)
   {
      this.realms = realms;
   }

   AcmeSession getSession(String id)
   {
      return (AcmeSession) sessions.get(id);
   }

   HttpSession createSession()
   {
      Integer ms = (Integer) this.arguments.get(ARG_MAX_ACTIVE_SESSIONS);
      if (ms != null && ms.intValue() < sessions.size())
         return null;
      HttpSession result = new AcmeSession(generateSessionId(), Math.abs(expiredIn) * 60, this, sessions);
      synchronized (sessions)
      {
         sessions.put(result.getId(), result);
      }
      return result;
   }

   void removeSession(String id)
   {
      synchronized (sessions)
      {
         sessions.remove(id);
      }
   }

   // / Write information to the servlet log.
   // @param message the message to log
   public void log(String message)
   {
      Date date = new Date(System.currentTimeMillis());
      logStream.println("[" + date.toString() + "] " + message);
   }

   public void log(String message, Throwable throwable)
   {
      if (throwable != null)
      {
         StringWriter sw;
         PrintWriter pw = new PrintWriter(sw = new StringWriter());
         throwable.printStackTrace(pw);
         // printCauses(throwable, pw);
         message = message + '\n' + sw;
      }
      log(message);
   }

   // protected void printCauses(Throwable throwable, PrintWriter printWriter) {
   // try {
   // throwable = throwable instanceof ServletException ? ((ServletException) throwable).getRootCause()
   // : (Throwable) throwable.getClass().getMethod("getCause", new Class[] {}).invoke(throwable,
   // new Object[] {});
   // if (throwable != null) {
   // printWriter.write("Caused by:\n");
   // throwable.printStackTrace(printWriter);
   // printCauses(throwable, printWriter);
   // }
   // } catch (Exception e) {
   // }
   // }

   // / Write a stack trace to the servlet log.
   // @param exception where to get the stack trace
   // @param message the message to log

   public void log(Exception exception, String message)
   {
      log(message, exception);
   }

   // / Applies alias rules to the specified virtual path and returns the
   // corresponding real path. It returns null if the translation
   // cannot be performed.
   // @param path the path to be translated
   public String getRealPath(String path)
   {
      // try {
      // path = new String(path.getBytes("ISO-8859-1"), UTF8);
      // } catch (Exception ee) { // no encoding
      // }
      // System.err.print("[" + path + "]->[");
      if (mappingtable != null)
      {
         // try find first sub-path
         Object[] os = mappingtable.get(path);
         // System.err.println("Searching for path: "+path+" found: "+os[0]);
         if (os[0] == null)
            return null;
         int slpos = ((Integer) os[1]).intValue();
         int pl = path.length();
         if (slpos > 0)
         {
            if (path.length() > slpos)
               path = path.substring(slpos + 1);
            else
               path = "";
         }
         else if (pl > 0)
         {
            for (int i = 0; i < pl; i++)
            {
               char s = path.charAt(i);
               if (s == '/' || s == '\\')
                  continue;
               else
               {
                  if (i > 0)
                     path = path.substring(i);
                  break;
               }
            }
         }
         // System.err.println("Path after processing :"+path+" slash was at
         // "+slpos);
         return new File((File) os[0], path).getPath();
      }
      return path;
   }

   /**
    * @return
    */
   public String getContextPath()
   {
      return "";
   }

   // / Returns the MIME type of the specified file.
   // @param file file name whose MIME type is required
   public String getMimeType(String file)
   {
      int dp = file.lastIndexOf('.');
      if (dp > 0)
      {
         return mime.getProperty(file.substring(dp + 1).toUpperCase());
      }
      return null;
   }

   // / Returns the name and version of the web server under which the servlet
   // is running.
   // Same as the CGI variable SERVER_SOFTWARE.
   public String getServerInfo()
   {
      return Serve.Identification.serverName + " " + Serve.Identification.serverVersion + " ("
              + Serve.Identification.serverUrl + ")";
   }

   // / Returns the value of the named attribute of the network service, or
   // null if the attribute does not exist. This method allows access to
   // additional information about the service, not already provided by
   // the other methods in this interface.
   public Object getAttribute(String name)
   {
      return attributes.get(name);
   }

   // ///////////////// JSDK 2.1 extensions //////////////////////////
   public void removeAttribute(String name)
   {
      attributes.remove(name);
   }

   public void setAttribute(String name, Object object)
   {
      if (object != null)
         attributes.put(name, object);
      else
         attributes.remove(name);
   }

   public Enumeration getAttributeNames()
   {
      return attributes.keys();
   }

   public ServletContext getContext(String uripath)
   {
      // TODO check webapp servlets to find out conexts for uri
      return this; // only root context supported
   }

   public int getMajorVersion()
   {
      return 2; // support 2.x
   }

   public int getMinorVersion()
   {
      return 5; // support 2.5
   }

   // 2.3

   /**
    * Returns a directory-like listing of all the paths to resources within the web application whose longest sub-path matches the supplied path argument.
    * Paths indicating subdirectory paths end with a '/'. The returned paths are all relative to the root of the web application and have a leading '/'. For
    * example, for a web application containing
    * <p/>
    * /welcome.html <br>
    * /catalog/index.html <br>
    * /catalog/products.html <br>
    * /catalog/offers/books.html <br>
    * /catalog/offers/music.html <br>
    * /customer/login.jsp <br>
    * /WEB-INF/web.xml <br>
    * /WEB-INF/classes/com.acme.OrderServlet.class,
    * <p/>
    * getResourcePaths("/") returns {"/welcome.html", "/catalog/", "/customer/", "/WEB-INF/"} <br>
    * getResourcePaths("/catalog/") returns {"/catalog/index.html", "/catalog/products.html", "/catalog/offers/"}.
    * <p/>
    *
    * @param the -
    *            partial path used to match the resources, which must start with a /
    * @return a Set containing the directory listing, or null if there are no resources in the web application whose path begins with the supplied path.
    * @since Servlet 2.3
    */
   public java.util.Set getResourcePaths(java.lang.String path)
   {
      String realPath = getRealPath(path);
      if (realPath != null)
      {

         String[] dir = new File(realPath).list();
         if (dir.length > 0)
         {
            HashSet set = new HashSet(dir.length);
            for (int i = 0; i < dir.length; i++)
               set.add(dir[i]);
            return set;
         }
      }
      return null;
   }

   /**
    * Returns the name of this web application correponding to this ServletContext as specified in the deployment descriptor for this web application by the
    * display-name element.
    *
    * @return The name of the web application or null if no name has been declared in the deployment descriptor.
    * @since Servlet 2.3
    */
   public java.lang.String getServletContextName()
   {
      return null;
   }

   /**
    * Returns a URL to the resource that is mapped to a specified path. The path must begin with a "/" and is interpreted as relative to the current context
    * root.
    * <p/>
    * <p/>
    * This method allows the servlet container to make a resource available to servlets from any source. Resources can be located on a local or remote file
    * system, in a database, or in a <code>.war</code> file.
    * <p/>
    * <p/>
    * The servlet container must implement the URL handlers and <code>URLConnection</code> objects that are necessary to access the resource.
    * <p/>
    * <p/>
    * This method returns <code>null</code> if no resource is mapped to the pathname.
    * <p/>
    * <p/>
    * Some containers may allow writing to the URL returned by this method using the methods of the URL class.
    * <p/>
    * <p/>
    * The resource content is returned directly, so be aware that requesting a <code>.jsp</code> page returns the JSP source code. Use a
    * <code>RequestDispatcher</code> instead to include results of an execution.
    * <p/>
    * <p/>
    * This method has a different purpose than <code>java.lang.Class.getResource</code>, which looks up resources based on a class loader. This method does
    * not use class loaders.
    *
    * @param path a <code>String</code> specifying the path to the resource
    * @return the resource located at the named path, or <code>null</code> if there is no resource at that path
    * @throws MalformedURLException if the pathname is not given in the correct form
    */
   public URL getResource(String path) throws MalformedURLException
   {
      if (path == null || path.length() == 0 || path.charAt(0) != '/')
         throw new MalformedURLException("Path " + path + " is not in acceptable form.");
      File resFile = new File(getRealPath(path));
      if (resFile.exists()) // TODO get canonical path is more robust
         return new URL("file", "localhost", resFile.getPath());
      return null;
   }

   /**
    * Returns the resource located at the named path as an <code>InputStream</code> object.
    * <p/>
    * <p/>
    * The data in the <code>InputStream</code> can be of any type or length. The path must be specified according to the rules given in
    * <code>getResource</code>. This method returns <code>null</code> if no resource exists at the specified path.
    * <p/>
    * <p/>
    * Meta-information such as content length and content type that is available via <code>getResource</code> method is lost when using this method.
    * <p/>
    * <p/>
    * The servlet container must implement the URL handlers and <code>URLConnection</code> objects necessary to access the resource.
    * <p/>
    * <p/>
    * This method is different from <code>java.lang.Class.getResourceAsStream</code>, which uses a class loader. This method allows servlet containers to
    * make a resource available to a servlet from any location, without using a class loader.
    *
    * @param path a <code>String</code> specifying the path to the resource
    * @return the <code>InputStream</code> returned to the servlet, or <code>null</code> if no resource exists at the specified path
    */
   public InputStream getResourceAsStream(String path)
   {
      try
      {
         return getResource(path).openStream();
      }
      catch (Exception e)
      {
      }
      return null;
   }

   public RequestDispatcher getRequestDispatcher(String urlpath)
   {
      if (urlpath == null || urlpath.length() == 0 || urlpath.charAt(0) != '/')
         return null;
      try
      {
         return new SimpleRequestDispatcher(urlpath);
      }
      catch (NullPointerException npe)
      {
         return null;
      }
   }

   // no way to specify parameters for context
   public String getInitParameter(String param)
   {
      return null;
   }

   public Enumeration getInitParameterNames()
   {
      return Utils.EMPTY_ENUMERATION;
   }

   public RequestDispatcher getNamedDispatcher(String name)
   {
      // named resources are not supported
      return null;
   }

   synchronized String generateSessionId()
   {
      srandom.nextBytes(uniqer);
      // TODO swap randomly bytes
      return Utils.base64Encode(uniqer);
   }

   protected class SimpleRequestDispatcher implements RequestDispatcher
   {
      HttpServlet servlet;

      String dispatchPath;

      String dispatchQuery;

      int dispatchLen;

      SimpleRequestDispatcher(String path)
      {
         Object[] os = registry.get(path);
         servlet = (HttpServlet) os[0];
         //log("Dispatch to: " + path + ", servlet "+servlet);
         if (servlet == null)
            throw new NullPointerException();
         dispatchLen = ((Integer) os[1]).intValue();
         int qmp = path.indexOf('?');
         if (qmp < 0 || qmp >= path.length() - 1)
            dispatchPath = path;
         else
         {
            dispatchPath = path.substring(0, qmp);
            dispatchQuery = path.substring(qmp + 1);
         }
      }

      public void forward(ServletRequest _request, ServletResponse _response) throws ServletException,
              java.io.IOException
      {
         _request.removeAttribute("javax.servlet.forward.request_uri"); // reset in case of nested
         _response.reset();
         servlet.service(new HttpServletRequestWrapper((HttpServletRequest) _request)
         {
            public java.lang.String getPathInfo()
            {
               return dispatchLen >= dispatchPath.length() ? null : dispatchPath.substring(dispatchLen);
            }

            public String getRequestURI()
            {
               return dispatchPath;
            }

            public String getQueryString()
            {
               return dispatchQuery;
            }

            public String getPathTranslated()
            {
               //System.out.println("Path t path i: "+getPathInfo()+", dp: "+dispatchPath);
               return getRequest().getRealPath(getPathInfo());
            }

            public String getServletPath()
            {
               return dispatchLen <= 0 ? "" : dispatchPath.substring(0, dispatchLen);
            }

            public synchronized java.util.Enumeration getAttributeNames()
            {
               if (super.getAttribute("javax.servlet.forward.request_uri") == null)
               {
                  setAttribute("javax.servlet.forward.request_uri", super.getRequestURI());
                  setAttribute("javax.servlet.forward.context_path", this.getContextPath());
                  setAttribute("javax.servlet.forward.servlet_path", super.getServletPath());
                  setAttribute("javax.servlet.forward.path_info", super.getPathInfo());
                  setAttribute("javax.servlet.forward.query_string", super.getQueryString());
               }
               return super.getAttributeNames();
            }

            public Object getAttribute(String name)
            {
               getAttributeNames(); // here is some overhead
               return super.getAttribute(name);
            }

         }, _response);
         // TODO think when response isn't actual response ServeConnection
         ((ServeConnection) _response).closeStreams(); // do not allow to continue
      }

      public void include(ServletRequest _request, ServletResponse _response) throws ServletException,
              java.io.IOException
      {
         _request.removeAttribute("javax.servlet.include.request_uri"); // reset in case of nested
         ((Serve.ServeConnection) _response).setInInclude(true);
         try
         {
            servlet.service(new HttpServletRequestWrapper((HttpServletRequest) _request)
            {
               public synchronized java.util.Enumeration getAttributeNames()
               {
                  if (super.getAttribute("javax.servlet.include.request_uri") == null)
                  {
                     setAttribute("javax.servlet.include.request_uri", dispatchPath);
                     setAttribute("javax.servlet.include.context_path", this.getContextPath());
                     setAttribute("javax.servlet.include.servlet_path", dispatchLen <= 0 ? "" : dispatchPath
                             .substring(0, dispatchLen));
                     setAttribute("javax.servlet.include.path_info", dispatchLen >= dispatchPath.length() ? null
                             : dispatchPath.substring(dispatchLen));
                     setAttribute("javax.servlet.include.query_string", dispatchQuery);
                  }
                  return super.getAttributeNames();
               }

               public Object getAttribute(String name)
               {
                  getAttributeNames(); // here is some overhead
                  return super.getAttribute(name);
               }

            }, _response);
         }
         finally
         {
            ((Serve.ServeConnection) _response).setInInclude(false);
         }
      }

   }

   // Keep Alive supporter, JDK 1.4 based for backwar compatibility
   class KeepAliveCleaner extends Thread
   {
      protected List connections;

      protected List ingoings;

      protected volatile boolean stopped;

      private boolean noCheckClose;

      KeepAliveCleaner()
      {
         super("KeepAlive cleaner");
         connections = new ArrayList();
         ingoings = new ArrayList();
         setDaemon(true);
      }

      public void end()
      {
         stopped = true;
         this.interrupt();
      }

      synchronized void addConnection(ServeConnection conn)
      {
         synchronized (ingoings)
         {
            if (stopped == false)
               ingoings.add(conn);
         }
      }

      public void run()
      {
         long d = getKeepAliveDuration();
         int maxUse = getMaxTimesConnectionUse();
         while (true)
         {
            synchronized (ingoings)
            {
               Iterator i = ingoings.iterator();
               while (i.hasNext())
               {
                  connections.add(i.next());
                  i.remove();
               }
            }
            Iterator i = connections.iterator();
            long ct = System.currentTimeMillis();
            d = getKeepAliveDuration();
            while (i.hasNext())
            {
               ServeConnection conn = (ServeConnection) i.next();
               boolean closed = conn.socket == null;
               if (noCheckClose == false)
                  synchronized (conn)
                  {
                     if (conn.socket != null)
                        try
                        {
                           closed = ((Boolean) conn.socket.getClass().getMethod("isClosed",
                                   Utils.EMPTY_CLASSES).invoke(conn.socket, Utils.EMPTY_OBJECTS))
                                   .booleanValue();
                        }
                        catch (IllegalArgumentException e)
                        {
                        }
                        catch (SecurityException e)
                        {
                        }
                        catch (IllegalAccessException e)
                        {
                        }
                        catch (InvocationTargetException e)
                        {
                        }
                        catch (NoSuchMethodException e)
                        {
                           noCheckClose = true;
                        }
                  }
               if (closed || (conn.keepAlive && (ct - conn.lastWait > d && conn.lastRun < conn.lastWait))
                       || stopped
                  /* || conn.timesRequested > maxUse */)
               {
                  i.remove();
                  synchronized (conn)
                  {
                     if (conn.socket != null)
                        try
                        {
                           //System.err.println("Closing socket:"+conn.socket.getClass().getName()); // !!!
                           //conn.socket.close();
                           conn.socket.getInputStream().close();
                        }
                        catch (IOException ioe)
                        {
                           // ignore
                        }
                     //System.err.println("done");
                  }
               }
            }
            if (stopped && connections.size() == 0)
               break;
            try
            {
               sleep(d);
            }
            catch (InterruptedException ie)
            {
               stopped = true; // not thread safe
            }
         }
      }
   }

   final static class Identification
   {
      public static final String serverName = "D. Rogatkin's TJWS based on Acme.Serve";

      public static final String serverVersion = "Version 1.70, $Revision: 1.194 $";

      public static final String serverUrl = "http://tjws.sourceforge.net";

      public static final String serverIdHtml = "<ADDRESS><A HREF=\"" + serverUrl + "\">" + serverName + " " + serverVersion + "</A></ADDRESS>";
   }

   // ////////////////////////////////////////////////////////////////

   protected static class ServeConfig implements ServletConfig
   {

      private ServletContext context;

      private Hashtable init_params;

      private String servletName;

      public ServeConfig(ServletContext context)
      {
         this(context, null, "undefined");
      }

      public ServeConfig(ServletContext context, Hashtable initParams, String servletName)
      {
         this.context = context;
         this.init_params = initParams;
         this.servletName = servletName;
      }

      // Methods from ServletConfig.

      // / Returns the context for the servlet.

      public ServletContext getServletContext()
      {
         return context;
      }

      // / Gets an initialization parameter of the servlet.
      // @param name the parameter name
      public String getInitParameter(String name)
      {
         // This server supports servlet init params. :)
         if (init_params != null)
            return (String) init_params.get(name);
         return null;
      }

      // / Gets the names of the initialization parameters of the servlet.
      // @param name the parameter name
      public Enumeration getInitParameterNames()
      {
         // This server does:) support servlet init params.
         if (init_params != null)
            return init_params.keys();
         return new Vector().elements();
      }

      // 2.2
      public String getServletName()
      {
         return servletName;
      }
   }

   // /////////////////////////////////////////////////////////////////////
   /**
    * provides request/response
    */
   public static class ServeConnection implements Runnable, HttpServletRequest, HttpServletResponse
   {
      private Socket socket;

      private Hashtable sslAttributes;

      private Serve serve;

      private ServletInputStream in;

      private ServletOutputStream out;

      private String scheme;

      public final static String WWWFORMURLENCODE = "application/x-www-form-urlencoded";

      public final static String TRANSFERENCODING = "transfer-encoding".toLowerCase();

      public final static String KEEPALIVE = "Keep-Alive".toLowerCase();

      public final static String CONTENT_ENCODING = "Content-Encoding".toLowerCase();

      public final static String CONNECTION = "Connection".toLowerCase();

      public final static String CHUNKED = "chunked";

      public final static String CONTENTLENGTH = "Content-Length".toLowerCase();

      public final static String CONTENTTYPE = "Content-Type".toLowerCase();

      public final static String SETCOOKIE = "Set-Cookie".toLowerCase();

      public final static String HOST = "Host".toLowerCase();

      public final static String COOKIE = "Cookie".toLowerCase();

      public final static String ACCEPT_LANGUAGE = "Accept-Language".toLowerCase();

      public final static String SESSION_COOKIE_NAME = "JSESSIONID";

      public final static String SESSION_URL_NAME = ";$sessionid$"; // ;jsessionid=

      private static final Map EMPTYHASHTABLE = new Hashtable();

      // URL rewriting
      // http://www.myserver.com/catalog/index.html;jsessionid=mysession1928
      // like:
      // http://www.sun.com/2001-0227/sunblade/;$sessionid$AD5RQ0IAADJAZAMTA1LU5YQ

      private String reqMethod; // == null by default

      private String reqUriPath, reqUriPathUn;

      private String reqProtocol;

      private String charEncoding; // req and resp

      private String remoteUser;

      private String authType;

      private boolean oneOne; // HTTP/1.1 or better

      private boolean reqMime;

      private Vector reqHeaderNames = new Vector();

      private Vector reqHeaderValues = new Vector();

      private Locale locale; // = java.util.Locale.getDefault();

      private int uriLen;

      protected boolean keepAlive = true;

      protected int timesRequested;

      protected long lastRun, lastWait;

      private Vector outCookies;

      private Vector inCookies;

      private String sessionCookieValue, sessionUrlValue, sessionValue;

      protected String reqQuery;

      private PrintWriter pw;

      private ServletOutputStream rout;

      private Map formParameters;

      private Hashtable attributes = new Hashtable();

      private int resCode = -1;

      private String resMessage;

      private Hashtable resHeaderNames = new Hashtable();

      private String[] postCache;

      private boolean headersWritten;

      private MessageFormat accessFmt;

      private Object[] logPlaceholders;

      // TODO consider creation an instance per thread in a pool, thread memory can be used

      private final SimpleDateFormat expdatefmt = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss 'GMT'", Locale.US); // used for cookie

      private final SimpleDateFormat rfc850DateFmt = new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss 'GMT'",
              Locale.US); // rfc850-date

      private final SimpleDateFormat headerdateformat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'",
              Locale.US); // rfc1123-date

      private final SimpleDateFormat asciiDateFmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy", Locale.US); // ASCII date, used in headers

      private static final TimeZone tz = TimeZone.getTimeZone("GMT");

      static
      {
         tz.setID("GMT");
      }

      /*
         * protected void finalize() throws Throwable { serve.log("Connection collected"); super.finalize(); }
         */

      // / Constructor.

      public ServeConnection(Socket socket, Serve serve)
      {
         // Save arguments.
         this.socket = socket;
         this.serve = serve;
         expdatefmt.setTimeZone(tz);
         headerdateformat.setTimeZone(tz);
         rfc850DateFmt.setTimeZone(tz);
         asciiDateFmt.setTimeZone(tz);
         if (serve.isAccessLogged())
         {
            // not format string must be not tull
            accessFmt = new MessageFormat((String) serve.arguments.get(ARG_ACCESS_LOG_FMT));
            logPlaceholders = new Object[12];
         }
         serve.threadPool.executeThread(this);
         synchronized(serve.connections)
         {
            serve.connections.add(this);
         }
      }

      private void initSSLAttrs()
      {
         if (socket.getClass().getName().indexOf("SSLSocket") > 0)
         {
            try
            {
               sslAttributes = new Hashtable();
               Object sslSession = socket.getClass().getMethod("getSession", Utils.EMPTY_CLASSES).invoke(socket,
                       Utils.EMPTY_OBJECTS);
               if (sslSession != null)
               {
                  sslAttributes.put("javax.net.ssl.session", sslSession);
                  Method m = sslSession.getClass().getMethod("getCipherSuite", Utils.EMPTY_CLASSES);
                  m.setAccessible(true);
                  sslAttributes.put("javax.net.ssl.cipher_suite", m.invoke(sslSession, Utils.EMPTY_OBJECTS));
                  m = sslSession.getClass().getMethod("getPeerCertificates", Utils.EMPTY_CLASSES);
                  m.setAccessible(true);
                  sslAttributes.put("javax.net.ssl.peer_certificates", m.invoke(sslSession, Utils.EMPTY_OBJECTS));
               }
            }
            catch (IllegalAccessException iae)
            {
               sslAttributes = null;
               //iae.printStackTrace();
            }
            catch (NoSuchMethodException nsme)
            {
               sslAttributes = null;
               //nsme.printStackTrace();
            }
            catch (InvocationTargetException ite)
            {
               // note we do not clear attributes, because SSLPeerUnverifiedException
               // happens in the last call, when no client sertificate
               //sslAttributes = null;
               //ite.printStackTrace();
            }
            catch (IllegalArgumentException iae)
            {
               //sslAttributes = null;
               //iae.printStackTrace();
            }
            //System.err.println("Socket SSL attrs: "+sslAttributes);
         }
      }

      /**
       * it closes stream awaring of keep -alive
       *
       * @throws IOException
       */
      public void closeStreams() throws IOException
      {
         //System.err.println("===>CLOSE()");
         IOException ioe = null;
         try
         {
            if (pw != null)
               pw.flush();
            else
               out.flush();
         }
         catch (IOException io1)
         {
            ioe = io1;
         }
         try
         {
            out.close();
         }
         catch (IOException io1)
         {
            if (ioe != null)
               ioe = (IOException) ioe.initCause(io1);
            else
               ioe = io1;
         }
         try
         {
            in.close();
         }
         catch (IOException io1)
         {
            if (ioe != null)
               ioe = (IOException) ioe.initCause(io1);
            else
               ioe = io1;
         }
         if (ioe != null)
            throw ioe;
      }

      public void closeSocket()
      {
         if (socket != null)
         {
            try
            {
               socket.close();
            }
            catch (IOException e)
            {
            }
         }
      }

      // protected void finalize() throws Throwable {
      // System.err.println("Connection object gone"); // !!!
      // super.finalize();
      // }

      private void restart()
      {
         // new Exception("RESTART").printStackTrace();
         reqMethod = null;
         reqUriPath = reqUriPathUn = null;
         reqProtocol = null;
         charEncoding = null;
         remoteUser = null;
         authType = null;
         oneOne = false;
         reqMime = false;
         // considering that clear() works faster than new
         if (reqHeaderNames == null)
            reqHeaderNames = new Vector();
         else
            reqHeaderNames.clear();
         if (reqHeaderValues == null)
            reqHeaderValues = new Vector();
         else
            reqHeaderValues.clear();
         locale = null;
         uriLen = 0;
         outCookies = null;
         inCookies = null;
         sessionCookieValue = null;
         sessionUrlValue = null;
         sessionValue = null;
         reqQuery = null;
         pw = null;
         rout = null;
         formParameters = null;
         if (attributes == null)
            attributes = new Hashtable();
         else
            attributes.clear();
         if (sslAttributes != null)
            attributes.putAll(sslAttributes);
         resCode = -1;
         resMessage = null;
         resHeaderNames.clear();
         headersWritten = false;
         postCache = null;
         ((ServeInputStream) in).refresh();
         ((ServeOutputStream) out).refresh();
      }

      // Methods from Runnable.
      public void run()
      {
         try
         {
            initSSLAttrs();
            in = new ServeInputStream(socket.getInputStream(), this);
            out = new ServeOutputStream(socket.getOutputStream(), this);
            do
            {
               restart();
               // Get the streams.
               parseRequest();
               if (reqMethod != null && serve.isAccessLogged())
               {
                  // consider caching socket stuff for faster logging
                  // {0} {1} {2} [{3,date,dd/MMM/yyyy:HH:mm:ss Z}] \"{4} {5} {6}\" {7,number,#} {8,number} {9} {10}
                  // ARG_ACCESS_LOG_FMT
                  logPlaceholders[0] = socket.getInetAddress(); // IP
                  logPlaceholders[1] = "-"; // the RFC 1413 identity of the client
                  logPlaceholders[2] = remoteUser == null ? "-" : remoteUser; // remote user
                  logPlaceholders[3] = new Date(lastRun); // time stamp {3,date,dd/MMM/yyyy:HH:mm:ss Z} {3,time,}
                  logPlaceholders[4] = reqMethod; // method
                  logPlaceholders[5] = reqUriPathUn; // resource
                  logPlaceholders[6] = reqProtocol; // protocol
                  logPlaceholders[7] = new Integer(resCode); // res code
                  logPlaceholders[8] = new Long(((ServeOutputStream) out).lengthWritten());
                  logPlaceholders[9] = new Integer(socket.getLocalPort());
                  logPlaceholders[10] = serve.isShowReferer() ? getHeader("Referer") : "-";
                  logPlaceholders[11] = serve.isShowUserAgent() ? getHeader("User-Agent") : "-";
                  serve.logStream.println(accessFmt.format(logPlaceholders));
               }
               lastRun = 0;
               timesRequested++;
            } while (keepAlive && serve.isKeepAlive() && timesRequested < serve.getMaxTimesConnectionUse());
         }
         catch (IOException ioe)
         {
            //System.err.println("Drop "+ioe);
            String errMsg = ioe.getMessage();
            if ((errMsg == null || errMsg.indexOf("ocket closed") < 0) && ioe instanceof java.nio.channels.AsynchronousCloseException == false)
               serve.log("IO error: " + ioe + " in processing a request from " + socket.getInetAddress() + ":"
                       + socket.getLocalPort() + " / " + socket.getClass().getName()/*, ioe*/);
            else
               synchronized (this)
               {
                  //serve.log("Exception considered as socket closed:"+ioe, ioe);
                  socket = null;
               }
         }
         finally
         {
            synchronized (this)
            {
               synchronized(serve.connections)
               {
                  serve.connections.remove(this);
               }
               if (socket != null)
                  try
                  {
                     socket.close();
                  }
                  catch (IOException e)
                  { /* ignore */
                  }
               socket = null;
            }
         }
      }

      private void parseRequest() throws IOException
      {
         byte[] lineBytes = new byte[4096];
         int len;
         String line;
         // / TODO put time mark here for start waiting for receiving requests
         lastWait = System.currentTimeMillis();
         // Read the first line of the request.
         len = in.readLine(lineBytes, 0, lineBytes.length);
         if (len == -1 || len == 0)
         {
            if (keepAlive)
            {
               keepAlive = false;
               // connection seems be closed

            }
            else
            {
               problem("Status-Code 400: Bad Request(empty)", SC_BAD_REQUEST);
            }
            return;
         }
         if (len >= lineBytes.length)
         {
            problem("Status-Code 414: Request-URI Too Long", SC_REQUEST_URI_TOO_LONG);
            return;
         }
         // //lastRun = 0; // to avoid closing socket in long process
         line = new String(lineBytes, 0, len, UTF8);
         StringTokenizer ust = new StringTokenizer(line);
         if (ust.hasMoreTokens())
         {
            reqMethod = ust.nextToken();
            if (ust.hasMoreTokens())
            {
               reqUriPathUn = ust.nextToken();
               // TODO make it only when URL overwrite enambled
               int uop = reqUriPathUn.indexOf(SESSION_URL_NAME);
               if (uop > 0)
               {
                  sessionUrlValue = reqUriPathUn.substring(uop + SESSION_URL_NAME.length());
                  reqUriPathUn = reqUriPathUn.substring(0, uop);
                  try
                  {
                     serve.getSession(sessionUrlValue).userTouch();
                  }
                  catch (NullPointerException npe)
                  {
                     sessionUrlValue = null;
                  }
                  catch (IllegalStateException ise)
                  {
                     sessionUrlValue = null;
                  }
               }
               if (ust.hasMoreTokens())
               {
                  reqProtocol = ust.nextToken();
                  oneOne = !reqProtocol.toUpperCase().equals("HTTP/1.0");
                  reqMime = true;
                  // Read the rest of the lines.
                  String s;
                  while ((s = ((ServeInputStream) in).readLine(HTTP_MAX_HDR_LEN)) != null)
                  {
                     if (s.length() == 0)
                        break;
                     int c = s.indexOf(':', 0);
                     if (c > 0)
                     {
                        String key = s.substring(0, c).trim().toLowerCase();
                        String value = s.substring(c + 1).trim();
                        reqHeaderNames.addElement(key);
                        reqHeaderValues.addElement(value);
                        if (CONNECTION.equalsIgnoreCase(key))
                           if (oneOne)
                              keepAlive = "close".equalsIgnoreCase(value) == false;
                           else
                              keepAlive = KEEPALIVE.equalsIgnoreCase(value);
                        else if (KEEPALIVE.equalsIgnoreCase(key))
                        { /// FF specific ?
                           // parse value to extract the connection specific timeoutKeepAlive and maxAliveConnUse
                           // todo that introduce the value in req/resp and copy defaults from Serve

                        }
                     }
                     else
                        serve.log("header field '" + s + "' without ':'");
                  }
               }
               else
               {
                  reqProtocol = "HTTP/0.9";
                  oneOne = false;
                  reqMime = false;
               }
            }
         }

         if (reqProtocol == null)
         {
            problem("Status-Code 400: Malformed request line:" + line, SC_BAD_REQUEST);
            return;
         }
         // Check Host: header in HTTP/1.1 requests.
         if (oneOne)
         {
            String host = getHeader(HOST);
            if (host == null)
            {
               problem("'Host' header missing in HTTP/1.1 request", SC_BAD_REQUEST);
               return;
            }
         }

         // Split off query string, if any.
         int qmark = reqUriPathUn.indexOf('?');
         if (qmark > -1)
         {
            if (qmark < reqUriPathUn.length() - 1)
               reqQuery = reqUriPathUn.substring(qmark + 1);
            reqUriPathUn = reqUriPathUn.substring(0, qmark);
         }
         reqUriPath = Utils.decode(reqUriPathUn, UTF8);
         // TDOD check if reqUriPathUn starts with http://host:port
         if (CHUNKED.equals(getHeader(TRANSFERENCODING)))
         {
            setHeader(CONTENTLENGTH, null);
            ((ServeInputStream) in).chunking(true);
         }
         String contentEncoding = extractEncodingFromContentType(getHeader(CONTENTTYPE));
         // TODO: encoding in request can be invalid, then do default
         setCharacterEncoding(contentEncoding != null ? contentEncoding : UTF8);
         String contentLength = getHeader(CONTENTLENGTH);
         if (contentLength != null)
            try
            {
               ((ServeInputStream) in).setContentLength(Long.parseLong(contentLength));
            }
            catch (NumberFormatException nfe)
            {
               serve.log("Invalid value of input content-length: " + contentLength);
            }
         // the code was originally in processing headers loop, however hhas been moved here
         /*  Don't do content encoding.  Let RESTEASY handle it
         String encoding = getHeader(CONTENT_ENCODING);
         if (encoding != null)
         {
            if ((encoding.equalsIgnoreCase("gzip") || encoding.equalsIgnoreCase("compressed"))
                    && null != serve.gzipInStreamConstr && ((ServeInputStream) in).compressed(true))
            {
            }
            else
            {
               problem("Status-Code 415: Unsupported media type:" + encoding, SC_UNSUPPORTED_MEDIA_TYPE);
               return;
            }
         }
         */
         if (assureHeaders() && socket.getKeepAlive() == false)
            socket.setKeepAlive(true);
         // TODO new SimpleRequestDispatcher(reqUriPathUn).forward((ServletRequest) this, (ServletResponse) this);
         Object[] os = serve.registry.get(reqUriPath);
         if (os[0] != null)
         { // note, os always not null
            // / TODO put time mark here to monitor actual servicing
            lastRun = System.currentTimeMillis();
            // System.err.println("Servlet "+os[0]+" for path "+reqUriPath);
            uriLen = ((Integer) os[1]).intValue();
            runServlet((HttpServlet) os[0]);
         }
         else
         {
            problem("No any servlet found for serving " + reqUriPath, SC_BAD_REQUEST);
         }
      }

      private boolean assureHeaders()
      {
         if (reqMime)
            setHeader("MIME-Version", "1.0");
         setDateHeader("Date", System.currentTimeMillis());
         setHeader("Server", Serve.Identification.serverName + "/" + Serve.Identification.serverVersion);
         if (keepAlive && serve.isKeepAlive())
         {
            if (reqMime)
            {
               setHeader(CONNECTION, KEEPALIVE); // set for 1.1 too, because some client do not follow a standard
               if (oneOne)
                  setHeader(KEEPALIVE, serve.getKeepAliveParamStr());
            }
            return true;
         }
         else
            setHeader(CONNECTION, "close");
         return false;
      }

      private void runServlet(HttpServlet servlete) throws IOException
      {
         // Set default response fields.
         setStatus(SC_OK);
         try
         {
            parseCookies();
            if (sessionValue == null) // not from cookie
               sessionValue = sessionUrlValue;
            if (authenificate())
            {
               if (servlete instanceof SingleThreadModel)
                  synchronized (servlete)
                  {
                     servlete.service((ServletRequest) this, (ServletResponse) this);
                  }
               else
                  servlete.service((ServletRequest) this, (ServletResponse) this);
            }
            // old close
         }
         catch (UnavailableException e)
         {
            if (e.isPermanent())
            {
               serve.registry.remove(servlete);
               servlete.destroy();
            }
            else if (e.getUnavailableSeconds() > 0)
               serve.log("Temporary unavailability feature is not supported " + servlete);
            problem(e.getMessage(), SC_SERVICE_UNAVAILABLE);
         }
         catch (ServletException e)
         {
            serve.log("Servlet exception", e);
            Throwable rootCause = e.getRootCause();
            while (rootCause != null)
            {
               serve.log("Caused by", rootCause);
               if (rootCause instanceof ServletException)
                  rootCause = ((ServletException) rootCause).getRootCause();
               else
                  rootCause = rootCause.getCause(); /* 1.4 */
            }
            problem(e.toString(), SC_INTERNAL_SERVER_ERROR);
         }
         catch (IOException ioe)
         {
            throw ioe;
         }
         catch (Exception e)
         {
            serve.log("Unexpected problem running servlet", e);
            problem("Unexpected problem running servlet: " + e.toString(), SC_INTERNAL_SERVER_ERROR);
         }
         finally
         {
            closeStreams();
            // socket will be closed by a caller if no keep-alive
         }
      }

      private boolean authenificate() throws IOException
      {
         Object[] o = serve.realms.get(reqUriPath); // by Niel Markwick
         BasicAuthRealm realm = null;
         if (o != null)
            realm = (BasicAuthRealm) o[0];
         // System.err.println("looking for realm for path "+getPathInfo()+"
         // in
         // "+serve.realms+" found "+realm);
         if (realm == null)
            return true;

         String credentials = getHeader("Authorization");

         if (credentials != null)
         {
            credentials = Acme.Utils.base64Decode(credentials.substring(credentials.indexOf(' ') + 1),
                    getCharacterEncoding());
            int i = credentials.indexOf(':');
            String user = credentials.substring(0, i);
            String password = credentials.substring(i + 1);
            remoteUser = user;
            authType = "BASIC"; // support only basic authenification (FORM, CLIENT_CERT, DIGEST )
            String realPassword = (String) realm.get(user);
            // System.err.println("User "+user+" Password "+password+" real
            // "+realPassword);
            if (realPassword != null && realPassword.equals(password))
               return true;
         }

         setStatus(SC_UNAUTHORIZED);
         setHeader("WWW-Authenticate", "basic realm=\"" + realm.name() + '"');
         //writeHeaders(); // because sendError() is used
         realSendError();
         return false;
      }

      private void problem(String logMessage, int resCode)
      {
         serve.log(logMessage);
         try
         {
            sendError(resCode, logMessage);
         }
         catch (IllegalStateException e)
         { /* ignore */
         }
         catch (IOException e)
         { /* ignore */
         }
      }

      private static final int MAYBEVERSION = 1;

      private static final int INVERSION = 2;

      private static final int OLD_INNAME = 3;

      private static final int OLD_INVAL = 4;

      private static final int INVERSIONNUM = 5;

      private static final int RECOVER = 6;

      private static final int NEW_INNAME = 7;

      private static final int NEW_INVAL = 8;

      private static final int INPATH = 9;

      private static final int MAYBEINPATH = 10;

      private static final int INPATHVALUE = 11;

      private static final int MAYBEPORT = 12;

      private static final int INDOMAIN = 13;

      private static final int MAYBEDOMAIN = 14;

      private static final int INPORT = 15;

      private static final int INDOMAINVALUE = 16;

      private static final int INPORTVALUE = 17;

      private void parseCookies() throws IOException
      {
         if (inCookies == null)
            inCookies = new Vector();
         String cookies = getHeader(COOKIE);
         if (cookies == null)
            return;
         try
         {
            String cookie_name = null;
            String cookie_value = null;
            String cookie_path = null;
            String cookie_domain = null;
            if (cookies.length() > 300 * 4096)
               throw new IOException("Cookie string too long:" + cookies.length());
            //System.err.println("We received:" + cookies);
            char[] cookiesChars = cookies.toCharArray();
            int state = MAYBEVERSION;
            StringBuffer token = new StringBuffer(256);
            boolean quoted = false;
            for (int i = 0; i < cookiesChars.length; i++)
            {
               char c = cookiesChars[i];

               switch (state)
               {
                  case MAYBEVERSION:
                     if (c != ' ')
                     {
                        token.append(c);
                        if (c == '$')
                        {
                           state = INVERSION; // RFC 2965
                        }
                        else
                           // RFC 2109
                           state = OLD_INNAME;
                     }
                     break;
                  case OLD_INNAME:
                     if (c == '=')
                     {
                        state = OLD_INVAL;
                        cookie_name = token.toString();
                        token.setLength(0);
                     }
                     else if (c != ' ' || token.length() > 0)
                        token.append(c);
                     break;
                  // TODO introduce val_start. then quoted value and value
                  case OLD_INVAL:
                     if (quoted == false)
                     {
                        if (c == ';')
                        {
                           state = OLD_INNAME;
                           cookie_value = token.toString();
                           token.setLength(0);
                           addCookie(cookie_name, cookie_value, null, null);
                        }
                        else if (c == '"' && token.length() == 0)
                           quoted = true;
                        else
                           token.append(c);
                     }
                     else
                     {
                        if (c == '"')
                           quoted = false;
                        else
                           token.append(c);
                     }
                     break;
                  case INVERSION:
                     if (c == '=')
                     {
                        if ("$Version".equals(token.toString()))
                           state = INVERSIONNUM;
                        else
                        {
                           state = OLD_INVAL; // consider name starts with $
                           cookie_name = token.toString();
                        }
                        token.setLength(0);
                     }
                     else
                        token.append(c);
                     break;
                  case INVERSIONNUM:
                     if (c == ',' || c == ';')
                     {
                        token.setLength(0);
                        state = NEW_INNAME;
                     }
                     else if (Character.isDigit(c) == false)
                     {
                        state = RECOVER;
                     }
                     else
                        token.append(c);
                     break;
                  case NEW_INNAME:
                     if (c == '=')
                     {
                        state = NEW_INVAL;
                        cookie_name = token.toString();
                        token.setLength(0);
                     }
                     else if (c != ' ' || token.length() > 0)
                        token.append(c);
                     break;
                  case NEW_INVAL:
                     if (c == ';')
                     {
                        state = MAYBEINPATH;
                        cookie_value = token.toString();
                        token.setLength(0);
                        cookie_path = null;
                     }
                     else if (c == ',')
                     {
                        state = NEW_INNAME;
                        cookie_value = token.toString();
                        token.setLength(0);
                        addCookie(cookie_name, cookie_value, null, null);
                     }
                     else
                        token.append(c);
                     break;
                  case MAYBEINPATH:
                     if (c != ' ')
                     {
                        token.append(c);
                        if (c == '$')
                        {
                           state = INPATH;
                        }
                        else
                        {
                           addCookie(cookie_name, cookie_value, null, null);
                           state = NEW_INNAME;
                        }
                     }
                     break;
                  case INPATH:
                     if (c == '=')
                     {
                        if ("$Path".equals(token.toString()))
                           state = INPATHVALUE;
                        else
                        {
                           addCookie(cookie_name, cookie_value, null, null);
                           state = NEW_INVAL; // consider name starts with $
                           cookie_name = token.toString();
                        }
                        token.setLength(0);
                     }
                     else
                        token.append(c);
                     break;
                  case INPATHVALUE:
                     if (c == ',')
                     {
                        cookie_path = token.toString();
                        state = NEW_INNAME;
                        addCookie(cookie_name, cookie_value, cookie_path, null);
                        token.setLength(0);
                     }
                     else if (c == ';')
                     {
                        state = MAYBEDOMAIN;
                        cookie_path = token.toString();
                        token.setLength(0);
                     }
                     else
                        token.append(c);
                     break;
                  case MAYBEDOMAIN:
                     if (c != ' ')
                     {
                        token.append(c);
                        if (c == '$')
                        {
                           state = INDOMAIN;
                        }
                        else
                        {
                           addCookie(cookie_name, cookie_value, cookie_path, null);
                           state = NEW_INNAME;
                        }
                     }
                     break;
                  case INDOMAIN:
                     if (c == '=')
                     {
                        if ("$Domain".equals(token.toString()))
                           state = INDOMAINVALUE;
                        else
                        {
                           addCookie(cookie_name, cookie_value, cookie_path, null);
                           state = NEW_INVAL; // consider name starts with $
                           cookie_name = token.toString();
                        }
                        token.setLength(0);
                     }
                     break;
                  case INDOMAINVALUE:
                     if (c == ',')
                     {
                        state = NEW_INNAME;
                        addCookie(cookie_name, cookie_value, cookie_path, token.toString());
                        token.setLength(0);
                     }
                     else if (c == ';')
                     {
                        cookie_domain = token.toString();
                        state = MAYBEPORT;
                     }
                     else
                        token.append(c);
                     break;
                  case MAYBEPORT:
                     if (c != ' ')
                     {
                        token.append(c);
                        if (c == '$')
                        {
                           state = INPORT;
                        }
                        else
                        {
                           addCookie(cookie_name, cookie_value, cookie_path, cookie_domain);
                           state = NEW_INNAME;
                        }
                     }
                     break;
                  case INPORT:
                     if (c == '=')
                     {
                        if ("$Port".equals(token.toString()))
                           state = INPORTVALUE;
                        else
                        {
                           addCookie(cookie_name, cookie_value, cookie_path, cookie_domain);
                           state = NEW_INVAL; // consider name starts with $
                           cookie_name = token.toString();
                        }
                        token.setLength(0);
                     }
                     break;
                  case INPORTVALUE:
                     if (c == ',' || c == ';')
                     {
                        int port = Integer.parseInt(token.toString());
                        state = NEW_INNAME;
                        addCookie(cookie_name, cookie_value, cookie_path, cookie_domain);
                        token.setLength(0);
                     }
                     else if (Character.isDigit(c) == false)
                     {
                        state = RECOVER;
                     }
                     else
                        token.append(c);
                     break;
                  case RECOVER:
                     serve.log("Parsing recover of cookie string " + cookies, null);
                     if (c == ';' || c == ',')
                     {
                        token.setLength(0);
                        state = NEW_INNAME;
                     }
                     break;
               }
            }
            if (state == OLD_INVAL || state == NEW_INVAL)
            {
               cookie_value = token.toString();
               addCookie(cookie_name, cookie_value, null, null);
            }
            else if (state == INPATHVALUE)
            {
               addCookie(cookie_name, cookie_value, token.toString(), null);
            }
            else if (state == INDOMAINVALUE)
            {
               addCookie(cookie_name, cookie_value, cookie_path, token.toString());
            }
            else if (state == INPORTVALUE)
               addCookie(cookie_name, cookie_value, cookie_path, cookie_domain);
         }
         catch (Error e)
         {
            serve.log("Error in parsing cookies: " + cookies, e);
         }
         catch (Exception e)
         {
            serve.log("An exception in parsing cookies: " + cookies, e);
         }
      }

      private void addCookie(String name, String value, String path, String domain)
      {
         if (SESSION_COOKIE_NAME.equals(name) && sessionCookieValue == null)
         {
            sessionCookieValue = value;
            try
            {
               serve.getSession(sessionCookieValue).userTouch();
               sessionValue = sessionCookieValue;
               sessionUrlValue = null;
            }
            catch (IllegalStateException ise)
            {
               sessionCookieValue = null;
            }
            catch (NullPointerException npe)
            {
               sessionCookieValue = null;
            }
         }
         else
         {
            Cookie c;
            inCookies.addElement(c = new Cookie(name, value));
            if (path != null)
            {
               c.setPath(path);
               if (domain != null)
                  c.setDomain(domain);
            }
         }
      }

      // Methods from ServletRequest.

      // / Returns the size of the request entity data, or -1 if not known.
      // Same as the CGI variable CONTENT_LENGTH.

      public int getContentLength()
      {
         return getIntHeader(CONTENTLENGTH);
      }

      // / Returns the MIME type of the request entity data, or null if
      // not known.
      // Same as the CGI variable CONTENT_TYPE.
      public String getContentType()
      {
         return getHeader(CONTENTTYPE);
      }

      // / Returns the protocol and version of the request as a string of
      // the form <protocol>/<major version>.<minor version>.
      // Same as the CGI variable SERVER_PROTOCOL.
      public String getProtocol()
      {
         return reqProtocol;
      }

      // / Returns the scheme of the URL used in this request, for example
      // "http", "https", or "ftp". Different schemes have different rules
      // for constructing URLs, as noted in RFC 1738. The URL used to create
      // a request may be reconstructed using this scheme, the server name
      // and port, and additional information such as URIs.
      public String getScheme()
      {
         if (scheme == null)
            // lazy stuf dlc
            synchronized (this)
            {
               if (scheme == null)
                  scheme = socket.getClass().getName().indexOf("SSLSocket") > 0 ? "https" : "http";
            }
         return scheme;
      }

      // / Returns the host name of the server as used in the <host> part of
      // the request URI.
      // Same as the CGI variable SERVER_NAME.
      public String getServerName()
      {
         String serverName;
         serverName = getHeader(HOST);
         if (serverName != null && serverName.length() > 0)
         {
            int colon = serverName.indexOf(':');
            if (colon >= 0)
            {
               if (colon < serverName.length())
                  serverName = serverName.substring(0, colon);
            }
         }

         if (serverName == null)
         {
            try
            {
               serverName = InetAddress.getLocalHost().getHostName();
            }
            catch (java.net.UnknownHostException ignore)
            {
               serverName = "127.0.0.0";
            }
         }

         int slash = serverName.indexOf("/");
         if (slash >= 0)
            serverName = serverName.substring(slash + 1);
         return serverName;
      }

      // / Returns the port number on which this request was received as used
      // in
      // the <port> part of the request URI.
      // Same as the CGI variable SERVER_PORT.
      public int getServerPort()
      {
         return socket.getLocalPort();
      }

      // / Returns the IP address of the agent that sent the request.
      // Same as the CGI variable REMOTE_ADDR.
      public String getRemoteAddr()
      {
         return socket.getInetAddress().getHostAddress();
      }

      // / Returns the fully qualified host name of the agent that sent the
      // request.
      // Same as the CGI variable REMOTE_HOST.
      public String getRemoteHost()
      {
         String result = socket.getInetAddress().getHostName();
         return result != null ? result : getRemoteAddr();
      }

      // / Applies alias rules to the specified virtual path and returns the
      // corresponding real path, or null if the translation can not be
      // performed for any reason. For example, an HTTP servlet would
      // resolve the path using the virtual docroot, if virtual hosting is
      // enabled, and with the default docroot otherwise. Calling this
      // method with the string "/" as an argument returns the document root.
      public String getRealPath(String path)
      {
         return serve.getRealPath(path);
      }

      // / Returns an input stream for reading request data.
      // @exception IllegalStateException if getReader has already been called
      // @exception IOException on other I/O-related errors
      public ServletInputStream getInputStream() throws IOException
      {
         synchronized (in)
         {
            if (((ServeInputStream) in).isReturnedAsReader())
               throw new IllegalStateException("Already returned as a reader.");
            ((ServeInputStream) in).setReturnedAsStream(true);
         }
         return in;
      }

      // / Returns a buffered reader for reading request data.
      // @exception UnsupportedEncodingException if the character set encoding
      // isn't supported
      // @exception IllegalStateException if getInputStream has already been
      // called
      // @exception IOException on other I/O-related errors
      public BufferedReader getReader()
      {
         synchronized (in)
         {
            if (((ServeInputStream) in).isReturnedAsStream())
               throw new IllegalStateException("Already returned as a stream.");
            ((ServeInputStream) in).setReturnedAsReader(true);
         }
         if (charEncoding != null)
            try
            {
               return new BufferedReader(new InputStreamReader(in, charEncoding));
            }
            catch (UnsupportedEncodingException uee)
            {
            }
         return new BufferedReader(new InputStreamReader(in));
      }

      private synchronized Map getParametersFromRequest()
      {
         Map result = null;
         if ("GET".equals(reqMethod))
         {
            if (reqQuery != null)
               try
               {
                  result = Acme.Utils.parseQueryString(reqQuery, charEncoding);
               }
               catch (IllegalArgumentException ex)
               {
                  serve.log("Exception " + ex + " at parsing 'get' data " + reqQuery);
               }
         }
         else if ("POST".equals(reqMethod))
         {
            String contentType = getContentType();
            if (contentType != null && WWWFORMURLENCODE.regionMatches(true, 0, contentType, 0, WWWFORMURLENCODE.length()))
            {
               if (postCache == null)
               {
                  postCache = new String[1];
                  InputStream is = null;
                  try
                  {
                     result = Acme.Utils.parsePostData(getContentLength(), is = getInputStream(), charEncoding,
                             postCache);
                  }
                  catch (Exception ex)
                  {
                     serve.log("Exception " + ex + " at parsing 'POST' data of length " + getContentLength());
                     // TODO propagate the exception ?
                     return EMPTYHASHTABLE;
                  }
               }
               else
                  result = Acme.Utils.parseQueryString(postCache[0], charEncoding);
               if (reqQuery != null && reqQuery.length() > 0)
                  result.putAll(Acme.Utils.parseQueryString(reqQuery, charEncoding));
            }
            else if (reqQuery != null)
               result = Acme.Utils.parseQueryString(reqQuery, charEncoding);
         }
         return result != null ? result : EMPTYHASHTABLE;
      }

      // / Returns the parameter names for this request.
      public synchronized Enumeration getParameterNames()
      {
         if (formParameters == null)
            formParameters = getParametersFromRequest();
         return ((Hashtable) formParameters).keys();
      }

      // / Returns the value of the specified query string parameter, or null
      // if not found.
      // @param name the parameter name
      public String getParameter(String name)
      {
         String[] params = getParameterValues(name);
         if (params == null || params.length == 0)
            return null;

         return params[0];
      }

      // / Returns the values of the specified parameter for the request as an
      // array of strings, or null if the named parameter does not exist.
      public synchronized String[] getParameterValues(String name)
      {
         if (formParameters == null)
            getParameterNames();

         return (String[]) formParameters.get(name);
      }

      // / Returns the value of the named attribute of the request, or null if
      // the attribute does not exist. This method allows access to request
      // information not already provided by the other methods in this
      // interface.
      public Object getAttribute(String name)
      {
         // System.err.println("!!!Get att orig:"+name+"="+attributes.get(name));
         return attributes.get(name);
      }

      // Methods from HttpServletRequest.

      // / Gets the array of cookies found in this request.

      public Cookie[] getCookies()
      {
         Cookie[] cookieArray = new Cookie[inCookies.size()];
         inCookies.copyInto(cookieArray);
         return cookieArray;
      }

      // / Returns the method with which the request was made. This can be
      // "GET",
      // "HEAD", "POST", or an extension method.
      // Same as the CGI variable REQUEST_METHOD.
      public String getMethod()
      {
         return reqMethod;
      }

      /**
       * ****************************************************************************************************************************************************
       * Returns the part of this request's URL from the protocol name up to the query string in the first line of the HTTP request. To reconstruct an URL
       * with a scheme and host, use HttpUtils.getRequestURL(javax.servlet.http.HttpServletRequest).
       */
      // / Returns the full request URI.
      public String getRequestURI()
      {
         return reqUriPathUn;
      }

      /**
       * Reconstructs the URL the client used to make the request. The returned URL contains a protocol, server name, port number, and server path, but it
       * does not include query string parameters. <br>
       * Because this method returns a StringBuffer, not a string, you can modify the URL easily, for example, to append query parameters.
       * <p/>
       * This method is useful for creating redirect messages and for reporting errors.
       *
       * @return a StringBuffer object containing the reconstructed URL
       * @since 2.3
       */
      public java.lang.StringBuffer getRequestURL()
      {
         int port = getServerPort();
         return new StringBuffer().append(getScheme()).append("://").append(getServerName()).append(
                 "https".equals(getScheme()) && port == 443 || port == 80 ? "" : ":" + String.valueOf(port)).append(
                 getRequestURI());
      }

      // / Returns the part of the request URI that referred to the servlet
      // being
      // invoked.
      // Analogous to the CGI variable SCRIPT_NAME.
      public String getServletPath()
      {
         // In this server, the entire path is regexp-matched against the
         // servlet pattern, so there's no good way to distinguish which
         // part refers to the servlet.
         return uriLen > 0 ? reqUriPath.substring(0, uriLen) : "";
      }

      // / Returns optional extra path information following the servlet path,
      // but
      // immediately preceding the query string. Returns null if not
      // specified.
      // Same as the CGI variable PATH_INFO.
      public String getPathInfo()
      {
         // In this server, the entire path is regexp-matched against the
         // servlet pattern, so there's no good way to distinguish which
         // part refers to the servlet.
         return uriLen >= reqUriPath.length() ? null : reqUriPath.substring(uriLen);
      }

      // / Returns extra path information translated to a real path. Returns
      // null if no extra path information was specified.
      // Same as the CGI variable PATH_TRANSLATED.
      public String getPathTranslated()
      {
         // In this server, the entire path is regexp-matched against the
         // servlet pattern, so there's no good way to distinguish which
         // part refers to the servlet.
         return getRealPath(getPathInfo());
      }

      // / Returns the query string part of the servlet URI, or null if not
      // known.
      // Same as the CGI variable QUERY_STRING.
      public String getQueryString()
      {
         return reqQuery;
      }

      // / Returns the name of the user making this request, or null if not
      // known.
      // Same as the CGI variable REMOTE_USER.
      public String getRemoteUser()
      {
         return remoteUser;
      }

      // / Returns the authentication scheme of the request, or null if none.
      // Same as the CGI variable AUTH_TYPE.
      public String getAuthType()
      {
         return authType;
      }

      // / Returns the value of a header field, or null if not known.
      // Same as the information passed in the CGI variabled HTTP_*.
      // @param name the header field name
      public String getHeader(String name)
      {
         int i = reqHeaderNames.indexOf(name.toLowerCase());
         if (i == -1)
            return null;
         return (String) reqHeaderValues.elementAt(i);
      }

      public int getIntHeader(String name)
      {
         String val = getHeader(name);
         if (val == null)
            return -1;
         return Integer.parseInt(val);
      }

      public long getDateHeader(String name)
      {
         String val = getHeader(name);
         if (val == null)
            return -1;
         try
         {
            return headerdateformat.parse(val).getTime();
         }
         catch (ParseException pe)
         {
            try
            {
               return rfc850DateFmt.parse(val).getTime();
            }
            catch (ParseException pe1)
            {
               try
               {
                  return asciiDateFmt.parse(val).getTime();
               }
               catch (ParseException pe3)
               {
                  throw new IllegalArgumentException("Value " + val
                          + " can't be converted to Date using any of formats: [" + headerdateformat.toPattern()
                          + "][ " + rfc850DateFmt.toPattern() + "][" + asciiDateFmt.toPattern());
               }
            }
         }
      }

      // / Returns an Enumeration of the header names.
      public Enumeration getHeaderNames()
      {
         return reqHeaderNames.elements();
      }

      // / Gets the current valid session associated with this request, if
      // create is false or, if necessary, creates a new session for the
      // request, if create is true.
      // <P>
      // Note: to ensure the session is properly maintained, the servlet
      // developer must call this method (at least once) before any output
      // is written to the response.
      // <P>
      // Additionally, application-writers need to be aware that newly
      // created sessions (that is, sessions for which HttpSession.isNew
      // returns true) do not have any application-specific state.
      public synchronized HttpSession getSession(boolean create)
      {
         HttpSession result = null;
         if (sessionValue != null)
         {
            result = serve.getSession(sessionValue);
            if (result != null && ((AcmeSession) result).isValid() == false)
            {
               serve.removeSession(sessionValue);
               result = null;
            }
            //System.err.println("^^^^^^^req sess: "+sessionValue+", found:"+result);
         }
         if (result == null && create)
         {
            result = serve.createSession();
            if (result != null)
            {
               sessionValue = result.getId();
            }
            else
               throw new RuntimeException("A session can't be created");
            //System.err.println("^~~~~~created: "+sessionValue);
         }
         return result;
      }

      // JSDK 2.1
      public HttpSession getSession()
      {
         return getSession(true);
      }

      public boolean isRequestedSessionIdFromURL()
      {
         return isRequestedSessionIdFromUrl();
      }

      // from ServletRequest
      public Enumeration getAttributeNames()
      {
         return attributes.keys();
      }

      /**
       * Stores an attribute in this request. Attributes are reset between requests.
       * This method is most often used in conjunction with RequestDispatcher.
       * <p>Attribute names should follow the same conventions as package names.
       * Names beginning with java.*, javax.*, and com.sun.*, are reserved for
       * use by Sun Microsystems. If the object passed in is null, the effect is
       * the same as calling removeAttribute(java.lang.String).
       * <p/>
       * It is warned that when the request is dispatched from the servlet resides
       * in a different web application by RequestDispatcher, the object set by
       * this method may not be correctly retrieved in the caller servlet.
       *
       * @param name - a String specifying the name of the attribute
       * @param o    - the Object to be stored
       */
      public void setAttribute(String key, Object o)
      {
         //System.err.println("!!!Set att orig:"+key+"="+o);
         //if ("javax.servlet.jsp.jspException".equals(key) && o instanceof Throwable)
         //((Throwable)o).printStackTrace();

         if (o != null)
            attributes.put(key, o);
         else
            attributes.remove(key);
      }

      // / Gets the session id specified with this request. This may differ
      // from the actual session id. For example, if the request specified
      // an id for an invalid session, then this will get a new session with
      // a new id.
      public String getRequestedSessionId()
      {
         return sessionValue;
      }

      // / Checks whether this request is associated with a session that is
      // valid in the current session context. If it is not valid, the
      // requested session will never be returned from the getSession
      // method.
      public boolean isRequestedSessionIdValid()
      {
         if (sessionValue != null)
         {
            AcmeSession session = serve.getSession(sessionValue);
            return (session != null && session.isValid());
         }
         return false;
      }

      /**
       * Checks whether the session id specified by this request came in as a cookie. (The requested session may not be one returned by the getSession
       * method.)
       */
      public boolean isRequestedSessionIdFromCookie()
      {
         return sessionCookieValue != null;
      }

      // / Checks whether the session id specified by this request came in as
      // part of the URL. (The requested session may not be the one returned
      // by the getSession method.)
      public boolean isRequestedSessionIdFromUrl()
      {
         return sessionUrlValue != null;
      }

      // Methods from ServletResponse.

      // / Sets the content length for this response.
      // @param length the content length

      public void setContentLength(int length)
      {
         if (length >= 0)
            setIntHeader(CONTENTLENGTH, length);
         else
            setHeader(CONTENTLENGTH, null);
      }

      // / Sets the content type for this response.
      // @param type the content type
      public void setContentType(String type)
      {
         setHeader(CONTENTTYPE, type != null ? type : "Unknown");
      }

      // / Returns an output stream for writing response data.
      public ServletOutputStream getOutputStream()
      {
         synchronized (out)
         {
            if (rout == null)
            {
               if (pw != null)
                  throw new IllegalStateException("Already returned as a writer");
               rout = out;
            }
         }
         return rout;
      }

      // / Returns a print writer for writing response data. The MIME type of
      // the response will be modified, if necessary, to reflect the character
      // encoding used, through the charset=... property. This means that the
      // content type must be set before calling this method.
      // @exception UnsupportedEncodingException if no such encoding can be
      // provided
      // @exception IllegalStateException if getOutputStream has been called
      // @exception IOException on other I/O errors
      public PrintWriter getWriter() throws IOException
      {
         synchronized (out)
         {
            if (pw == null)
            {
               if (rout != null)
                  throw new IllegalStateException("Already was returned as servlet output stream");
               String encoding = getCharacterEncoding();
               if (encoding != null)
                  pw = new PrintWriter(new OutputStreamWriter(out, encoding));
               else
                  pw = new PrintWriter(out);
            }
         }
         return pw;
      }

      // / Returns the character set encoding used for this MIME body. The
      // character encoding is either the one specified in the assigned
      // content type, or one which the client understands. If no content
      // type has yet been assigned, it is implicitly set to text/plain.
      public String getCharacterEncoding()
      {
         String ct = (String) resHeaderNames.get(CONTENTTYPE.toLowerCase());
         if (ct != null)
         {
            String enc = extractEncodingFromContentType(ct);
            if (enc != null)
               return enc;
         }
         return charEncoding;
      }

      private String extractEncodingFromContentType(String ct)
      {
         if (ct == null)
            return null;
         int scp = ct.indexOf(';');
         if (scp > 0)
         {
            scp = ct.toLowerCase().indexOf("charset=", scp);
            if (scp >= 0)
            {
               ct = ct.substring(scp + "charset=".length()).trim();
               scp = ct.indexOf(';');
               if (scp > 0)
                  ct = ct.substring(0, scp);
               int l = ct.length();
               if (l > 2 && ct.charAt(0) == '"')
                  return ct.substring(1, l - 1);
               return ct;
            }
         }
         return null;
      }

      // 2.2
      // do not use buffer
      public void flushBuffer() throws java.io.IOException
      {
         ((ServeOutputStream) out).flush();
      }

      /**
       * Clears the content of the underlying buffer in the response without clearing headers or status code. If the response has been committed, this method
       * throws an IllegalStateException.
       *
       * @since 2.3
       */
      public void resetBuffer()
      {
         ((ServeOutputStream) out).reset();
         synchronized (this)
         {
            headersWritten = false;
         }
      }

      public int getBufferSize()
      {
         return ((ServeOutputStream) out).getBufferSize();
      }

      public void setBufferSize(int size)
      {
         ((ServeOutputStream) out).setBufferSize(size);
      }

      /**
       * Returns a boolean indicating if the response has been committed. A commited response has already had its status code and headers written.
       *
       * @return a boolean indicating if the response has been committed
       * @see setBufferSize(int), getBufferSize(), flushBuffer(), reset()
       */
      // a caller should think about syncronization
      public boolean isCommitted()
      {
         return headersWritten && ((ServeOutputStream) out).lengthWritten() > 0;
      }

      /**
       * Clears any data that exists in the buffer as well as the status code and headers. If the response has been committed, this method throws an
       * IllegalStateException.
       *
       * @throws java.lang.IllegalStateException
       *          -
       *          if the response has already been committed
       * @see setBufferSize(int), getBufferSize(), flushBuffer(), isCommitted()
       */
      public void reset() throws IllegalStateException
      {
         // new Exception("RESET").printStackTrace();
         if (!isCommitted())
         {
            if (outCookies != null)
               outCookies.clear();
            resHeaderNames.clear();
            pw = null;
            rout = null;
            ((ServeOutputStream) out).reset();
            assureHeaders();
         }
         else
            throw new IllegalStateException("Header have already been committed.");
      }

      /**
       * Sets the locale of the response, setting the headers (including the Content-Type's charset) as appropriate. This method should be called before a
       * call to getWriter(). By default, the response locale is the default locale for the server.
       *
       * @param loc -
       *            the locale of the response
       * @see getLocale()
       */
      public void setLocale(java.util.Locale locale)
      {
         this.locale = locale;
      }

      /**
       * For request: Returns the preferred Locale that the client will accept content in, based on the Accept-Language header. If the client request doesn't
       * provide an Accept-Language header, this method returns the default locale for the server.
       * <p/>
       * For response: Returns the locale specified for this response using the setLocale(java.util.Locale) method. Calls made to setLocale after the response
       * is committed have no effect. If no locale has been specified, the container's default locale is returned.
       */
      public java.util.Locale getLocale()
      {
         if (locale != null)
            return locale;
         Enumeration e = getLocales();
         if (e.hasMoreElements())
            return (Locale) e.nextElement();
         return Locale.getDefault();
      }

      /**
       * Returns an Enumeration of Locale objects indicating, in decreasing order starting with the preferred locale, the locales that are acceptable to the
       * client based on the Accept-Language header. If the client request doesn't provide an Accept-Language header, this method returns an Enumeration
       * containing one Locale, the default locale for the server.
       */
      public Enumeration getLocales()
      {
         // TODO: cache result
         String al = getHeader(ACCEPT_LANGUAGE);
         TreeSet ts = new TreeSet();
         if (al != null)
         {
            // System.err.println("Accept lang:"+al);
            StringTokenizer st = new StringTokenizer(al, ";", false);
            try
            {
               while (st.hasMoreTokens())
               {
                  String langs = st.nextToken(";");
                  // System.err.println("Langs:"+langs);
                  String q = st.nextToken(";=");
                  // System.err.println("q:"+q);
                  q = st.nextToken("=,");
                  // System.err.println("q:"+q);
                  float w = 0;
                  try
                  {
                     w = Float.valueOf(q).floatValue();
                  }
                  catch (NumberFormatException nfe)
                  {
                  }
                  if (w > 0)
                  {
                     StringTokenizer lst = new StringTokenizer(langs, ", ", false);
                     while (lst.hasMoreTokens())
                     {
                        String lan = lst.nextToken();
                        int di = lan.indexOf('-');
                        if (di < 0)
                           ts.add(new LocaleWithWeight(new Locale(lan.trim()) /* 1.4 */, w));
                        else
                           ts.add(new LocaleWithWeight(new Locale(lan.substring(0, di), lan.substring(di + 1)
                                   .trim().toUpperCase()), w));
                     }
                  }
               }
            }
            catch (NoSuchElementException ncee)
            {
               // can't parse
            }
         }
         if (ts.size() == 0)
            ts.add(new LocaleWithWeight(Locale.getDefault(), 1));
         return new AcceptLocaleEnumeration(ts);
      }

      /**
       * Overrides the name of the character encoding used in the body of this request. This method must be called prior to reading request parameters or
       * reading input using getReader().
       *
       * @param a -
       *          String containing the name of the chararacter encoding.
       * @throws java.io.UnsupportedEncodingException
       *          -
       *          if this is not a valid encoding
       * @since JSDK 2.3
       */
      public void setCharacterEncoding(String _enc)
      {
         // TODO: check if encoding is valid
         charEncoding = _enc;
         synchronized (this)
         {
            formParameters = null;
         }
      }

      public void addDateHeader(String header, long date)
      {
         addHeader(header, headerdateformat.format(new Date(date)));
      }

      public void addHeader(String header, String value)
      {
         header = header.trim().toLowerCase();
         Object o = resHeaderNames.get(header);
         if (o == null)
            setHeader(header, value);
         else
         {
            if (o instanceof String[])
            {
               String[] oldVal = (String[]) o;
               String[] newVal = new String[oldVal.length + 1];
               System.arraycopy(oldVal, 0, newVal, 0, oldVal.length);
               newVal[oldVal.length] = value;
               resHeaderNames.put(header, newVal);
            }
            else if (o instanceof String)
            {
               String[] newVal = new String[2];
               newVal[0] = (String) o;
               newVal[1] = value;
               resHeaderNames.put(header, newVal);
            }
            else
               throw new RuntimeException("Invalid content of header hash - " + o.getClass().getName());
         }
      }

      public void addIntHeader(String header, int value)
      {
         addHeader(header, Integer.toString(value));
      }

      public RequestDispatcher getRequestDispatcher(String urlpath)
      {
         if (urlpath.length() > 0 && urlpath.charAt(0) != '/')
         {
            String dispatchPath = getContextPath();
            String pathInfo = getPathInfo();
            String servletPath = getServletPath();
            ;
            if (pathInfo != null)
            {
               dispatchPath += servletPath;
               int slp = pathInfo.indexOf('/', 1);
               if (slp > 0) // can it ever happen?
                  dispatchPath += pathInfo.substring(0, slp - 1);
            }
            else
            {
               int spsp = servletPath.lastIndexOf('/');
               if (spsp >= 0)
                  dispatchPath += servletPath.substring(0, spsp);
            }
            // serve.log("Dispatch path:"+dispatchPath);
            urlpath = dispatchPath + '/' + urlpath;
         }
         return serve.getRequestDispatcher(urlpath);
      }

      public boolean isSecure()
      {
         return "https".equals(getScheme());
      }

      public void removeAttribute(String name)
      {
         attributes.remove(name);
      }

      // only root context supported
      public String getContextPath()
      {
         return "";
      }

      public Enumeration getHeaders(String header)
      {
         Vector result = new Vector();
         int i = -1;
         while ((i = reqHeaderNames.indexOf(header.toLowerCase(), i + 1)) >= 0)
            result.addElement(reqHeaderValues.elementAt(i));
         return result.elements();
      }

      public java.security.Principal getUserPrincipal()
      {
         return null;
      }

      public boolean isUserInRole(String user)
      {
         return false;
      }

      /**
       * Returns a java.util.Map of the parameters of this request. Request parameters are extra information sent with the request. For HTTP servlets,
       * parameters are contained in the query string or posted form data.
       *
       * @return an immutable java.util.Map containing parameter names as keys and parameter values as map values. The keys in the parameter map are of type
       *         String. The values in the parameter map are of type String array.
       * @since 2.3
       */
      public synchronized java.util.Map getParameterMap()
      {
         if (formParameters == null)
            getParameterNames();
         return formParameters;
      }

      // Methods from HttpServletResponse.

      // / Adds the specified cookie to the response. It can be called
      // multiple times to set more than one cookie.

      public void addCookie(Cookie cookie)
      {
         if (outCookies == null)
            outCookies = new Vector();

         outCookies.addElement(cookie);
      }

      // / Checks whether the response message header has a field with the
      // specified name.
      public boolean containsHeader(String name)
      {
         return resHeaderNames.contains(name);
      }

      // JSDK 2.1 extension
      public String encodeURL(String url)
      {
         int uop = url.indexOf(SESSION_URL_NAME);
         // TODO not robust enough
         if (uop > 0)
            url = url.substring(0, uop);
         if (sessionValue == null || isRequestedSessionIdFromCookie())
            return url;
         try
         {
            new URL(url); // for testing syntac
            int ehp = url.indexOf('/');
            if (ehp < 0)
               ehp = url.indexOf('?');
            if (ehp < 0)
               ehp = url.indexOf('#');
            if (ehp < 0)
               ehp = url.length();
            if (url.regionMatches(true, 0, getRequestURL().toString(), 0, ehp) == false)
               return url;
         }
         catch (MalformedURLException e)
         {
         }

         return url + SESSION_URL_NAME + sessionValue;
      }

      public String encodeRedirectURL(String url)
      {
         return encodeURL(url);
      }

      /**
       * Returns the Internet Protocol (IP) source port of the client or last proxy that sent the request.
       *
       * @return an integer specifying the port number
       * @since 2.4
       */
      public int getRemotePort()
      {
         return socket.getPort(); // TODO not quite robust
      }

      /**
       * Returns the host name of the Internet Protocol (IP) interface on which the request was received.
       *
       * @return a <code>String</code> containing the host name of the IP on which the request was received.
       * @since 2.4
       */
      public String getLocalName()
      {
         InetAddress ia = socket/* serve.serverSocket */.getLocalAddress();
         return ia == null ? null : ia.getHostAddress();
      }

      /**
       * Returns the Internet Protocol (IP) address of the interface on which the request was received.
       *
       * @return a <code>String</code> containing the IP address on which the request was received.
       * @since 2.4
       */
      public String getLocalAddr()
      {
         InetAddress ia = /* serve.serverSocket */socket.getLocalAddress();
         return ia == null ? null : ia.getCanonicalHostName(); /* 1.4 */
      }

      /**
       * Returns the Internet Protocol (IP) port number of the interface on which the request was received.
       *
       * @return an integer specifying the port number
       * @since 2.4
       */
      public int getLocalPort()
      {
         return getServerPort();
      }

      // / Sets the status code and message for this response.
      // @param resCode the status code
      // @param resMessage the status message
      public void setStatus(int resCode, String resMessage)
      {
         // if (((ServeOutputStream) out).isInInclude())
         // return;
         this.resCode = resCode;
         this.resMessage = resMessage;
      }

      // / Sets the status code and a default message for this response.
      // @param resCode the status code
      public void setStatus(int resCode)
      {
         switch (resCode)
         {
            case SC_CONTINUE:
               setStatus(resCode, "Continue");
               break;
            case SC_SWITCHING_PROTOCOLS:
               setStatus(resCode, "Switching protocols");
               break;
            case SC_OK:
               setStatus(resCode, "Ok");
               break;
            case SC_CREATED:
               setStatus(resCode, "Created");
               break;
            case SC_ACCEPTED:
               setStatus(resCode, "Accepted");
               break;
            case SC_NON_AUTHORITATIVE_INFORMATION:
               setStatus(resCode, "Non-authoritative");
               break;
            case SC_NO_CONTENT:
               setStatus(resCode, "No content");
               break;
            case SC_RESET_CONTENT:
               setStatus(resCode, "Reset content");
               break;
            case SC_PARTIAL_CONTENT:
               setStatus(resCode, "Partial content");
               break;
            case SC_MULTIPLE_CHOICES:
               setStatus(resCode, "Multiple choices");
               break;
            case SC_MOVED_PERMANENTLY:
               setStatus(resCode, "Moved permanentently");
               break;
            case SC_MOVED_TEMPORARILY:
               setStatus(resCode, "Moved temporarily");
               break;
            case SC_SEE_OTHER:
               setStatus(resCode, "See other");
               break;
            case SC_NOT_MODIFIED:
               setStatus(resCode, "Not modified");
               break;
            case SC_USE_PROXY:
               setStatus(resCode, "Use proxy");
               break;
            case SC_BAD_REQUEST:
               setStatus(resCode, "Bad request");
               break;
            case SC_UNAUTHORIZED:
               setStatus(resCode, "Unauthorized");
               break;
            case SC_PAYMENT_REQUIRED:
               setStatus(resCode, "Payment required");
               break;
            case SC_FORBIDDEN:
               setStatus(resCode, "Forbidden");
               break;
            case SC_NOT_FOUND:
               setStatus(resCode, "Not found");
               break;
            case SC_METHOD_NOT_ALLOWED:
               setStatus(resCode, "Method not allowed");
               break;
            case SC_NOT_ACCEPTABLE:
               setStatus(resCode, "Not acceptable");
               break;
            case SC_PROXY_AUTHENTICATION_REQUIRED:
               setStatus(resCode, "Proxy auth required");
               break;
            case SC_REQUEST_TIMEOUT:
               setStatus(resCode, "Request timeout");
               break;
            case SC_CONFLICT:
               setStatus(resCode, "Conflict");
               break;
            case SC_GONE:
               setStatus(resCode, "Gone");
               break;
            case SC_LENGTH_REQUIRED:
               setStatus(resCode, "Length required");
               break;
            case SC_PRECONDITION_FAILED:
               setStatus(resCode, "Precondition failed");
               break;
            case SC_REQUEST_ENTITY_TOO_LARGE:
               setStatus(resCode, "Request entity too large");
               break;
            case SC_REQUEST_URI_TOO_LONG:
               setStatus(resCode, "Request URI too long");
               break;
            case SC_UNSUPPORTED_MEDIA_TYPE:
               setStatus(resCode, "Unsupported media type");
               break;
            case SC_INTERNAL_SERVER_ERROR:
               setStatus(resCode, "Internal server error");
               break;
            case SC_NOT_IMPLEMENTED:
               setStatus(resCode, "Not implemented");
               break;
            case SC_BAD_GATEWAY:
               setStatus(resCode, "Bad gateway");
               break;
            case SC_SERVICE_UNAVAILABLE:
               setStatus(resCode, "Service unavailable");
               break;
            case SC_GATEWAY_TIMEOUT:
               setStatus(resCode, "Gateway timeout");
               break;
            case SC_HTTP_VERSION_NOT_SUPPORTED:
               setStatus(resCode, "HTTP version not supported");
               break;
            case 207:
               setStatus(resCode, "Multi Status");
               break;
            default:
               setStatus(resCode, "");
               break;
         }
      }

      // / Sets the value of a header field.
      // @param name the header field name
      // @param value the header field value
      public void setHeader(String header, String value)
      {
         header = header.trim().toLowerCase(); // normilize header
         if (value == null)
            resHeaderNames.remove(header);
         else
         {
            resHeaderNames.put(header, value);
            //if (header.equals(CONTENTTYPE)) {
            //	String enc = extractEncodingFromContentType(value);
            //	if (enc != null)
            //		setCharacterEncoding(enc);
            //}
         }
      }

      // / Sets the value of an integer header field.
      // @param name the header field name
      // @param value the header field integer value
      public void setIntHeader(String header, int value)
      {
         setHeader(header, Integer.toString(value));
      }

      // / Sets the value of a long header field.
      // @param name the header field name
      // @param value the header field long value
      public void setLongHeader(String header, long value)
      {
         setHeader(header, Long.toString(value));
      }

      // / Sets the value of a date header field.
      // @param name the header field name
      // @param value the header field date value
      public void setDateHeader(String header, long value)
      {
         setHeader(header, headerdateformat.format(new Date(value)));
      }

      // / Writes the status line and message headers for this response to the
      // output stream.
      // @exception IOException if an I/O error has occurred
      void writeHeaders() throws IOException
      {
         synchronized (this)
         {
            // TODO: possible to write trailer when chunked out,
            // so chunked out should be global flag
            if (headersWritten)
               return;

            headersWritten = true;
         }
         if (reqMime)
         {
            boolean chunked_out = false;
            boolean wasContentLen = false;
            if (resMessage.length() < 256)
               out.println(reqProtocol + " " + resCode + " " + resMessage.replace('\r', '/').replace('\n', '/'));
            else
               out.println(reqProtocol + " " + resCode + " "
                       + resMessage.substring(0, 255).replace('\r', '/').replace('\n', '/'));
            Enumeration he = resHeaderNames.keys();
            while (he.hasMoreElements())
            {
               String name = (String) he.nextElement();
               Object o = resHeaderNames.get(name);
               if (o instanceof String)
               {
                  String value = (String) o;
                  if (value != null)
                  {// just in case
                     out.println(name + ": " + value);
                     if (wasContentLen == false)
                        if (CONTENTLENGTH.equals(name))
                           try
                           {
                              wasContentLen = Long.parseLong(value) > 0;
                           }
                           catch (NumberFormatException nfe)
                           {
                           }
                     if (chunked_out == false)
                        if (TRANSFERENCODING.equals(name) && CHUNKED.equals(value))
                           chunked_out = true;
                  }
               }
               else if (o instanceof String[])
               {
                  String[] values = (String[]) o;
                  out.print(name + ": " + values[0]);
                  for (int i = 1; i < values.length; i++)
                     out.print("," + values[i]);
                  out.println();
               }
            }
            StringBuffer sb = null;
            StringBuffer sb2 = null;
            Cookie cc = null;
            // add session cookie
            if (sessionValue != null)
            {
               HttpSession session = serve.getSession(sessionValue);
               if (session != null)
               {
                  if (((AcmeSession) session).isValid())
                  {
                     if (session.isNew())
                     {
                        cc = new Cookie(SESSION_COOKIE_NAME, sessionValue);
                        if (serve.expiredIn < 0)
                           cc.setMaxAge(Math.abs(serve.expiredIn) * 60);
                        ServletContext sc = ((AcmeSession) session).getServletContext();
                        try
                        {
                           String cp = (String) sc.getClass().getMethod("getContextPath", Utils.EMPTY_CLASSES)
                                   .invoke(sc, Utils.EMPTY_OBJECTS);
                           if (cp.length() == 0)
                              cp = "/";
                           cc.setPath(cp);
                        }
                        catch (Exception e)
                        {

                        }

                        addCookie(cc);
                     }
                  }
                  else
                  {
                     cc = new Cookie(SESSION_COOKIE_NAME, "");
                     cc.setMaxAge(0);
                     addCookie(cc);
                  }
               }
            }

            // how to remove a cookie
            // cc = new Cookie(cookieName, "");
            // cc.setMaxAge(0);
            //
            for (int i = 0; outCookies != null && i < outCookies.size(); i++)
            {
               cc = (Cookie) outCookies.elementAt(i);
               if (cc.getSecure() && isSecure() == false)
                  continue;
               int version = cc.getVersion();
               String token;
               if (version > 1)
               {
                  if (sb2 == null)
                     sb2 = new StringBuffer(SETCOOKIE + "2: ");
                  else
                     sb2.append(',');
                  sb2.append(cc.getName());
                  sb2.append("=\"");
                  sb2.append(cc.getValue()).append('"');
                  token = cc.getComment();
                  if (token != null)
                     sb2.append("; Comment=\"").append(token).append('"');
                  token = cc.getDomain();
                  if (token != null)
                     sb2.append("; Domain=\"").append(token).append('"');
                  if (cc.getMaxAge() >= 0)
                     sb2.append("; Max-Age=\"").append(cc.getMaxAge()).append('"');
                  token = cc.getPath();
                  if (token != null)
                     sb2.append("; Path=\"").append(token).append('"');
                  if (cc.getSecure())
                  {
                     sb2.append("; Secure");
                  }
                  sb2.append("; Version=\"").append(version).append('"');
               }
               else
               {
                  if (sb == null)
                     sb = new StringBuffer(SETCOOKIE + ": ");
                  else
                     //sb.append(',');
                     sb.append("\r\n" + SETCOOKIE + ": "); // for IE not
                  sb.append(cc.getName());
                  sb.append('=');
                  sb.append(cc.getValue());//.append('"');
                  if (cc.getDomain() != null && cc.getDomain().length() > 0)
                  {
                     sb.append("; domain=" + cc.getDomain());
                  }
                  if (cc.getMaxAge() >= 0)
                  {
                     sb.append("; expires=");
                     sb.append(expdatefmt.format(new Date(new Date().getTime() + 1000l * cc.getMaxAge())));
                  }
                  if (cc.getPath() != null && cc.getPath().length() > 0)
                  {
                     sb.append("; path=" + cc.getPath());
                  }
                  if (cc.getSecure())
                  {
                     sb.append("; secure");
                  }
               }
            }
            if (sb != null)
            {
               out.println(sb.toString());
               //System.err.println("We sent cookies: " + sb);
            }
            if (sb2 != null)
            {
               out.println(sb2.toString());
               //System.err.println("We sent cookies 2: " + sb2);
            }
            // Test for existence of message body
            if (containsBody() && wasContentLen == false && chunked_out == false && serve.isKeepAlive())
            {
               out.println(TRANSFERENCODING + ": " + CHUNKED);
               chunked_out = true;
            }
            out.println();
            out.flush();
            ((ServeOutputStream) out).setChunked(chunked_out);
         }
      }
      
      private boolean containsBody()
      {
         return !("HEAD".equalsIgnoreCase(reqMethod) || (100 <= resCode && resCode < 200) || resCode == 204 || resCode == 304);
      }

      // / Writes an error response using the specified status code and
      // message.
      // @param resCode the status code
      // @param resMessage the status message
      // @exception IOException if an I/O error has occurred
      public void sendError(int resCode, String resMessage) throws IOException
      {
         setStatus(resCode, resMessage);
         realSendError();
      }

      // / Writes an error response using the specified status code and a
      // default
      // message.
      // @param resCode the status code
      // @exception IOException if an I/O error has occurred
      public void sendError(int resCode) throws IOException
      {
         setStatus(resCode);
         realSendError();
      }

      public void setInInclude(boolean set)
      {
         ((ServeOutputStream) out).setInInclude(set);
      }

      private void realSendError() throws IOException
      {
         if (isCommitted())
            throw new IllegalStateException("Can not send an error, headers have been already written");
         // if (((ServeOutputStream) out).isInInclude()) // ignore
         // return;
         setContentType("text/html");
         StringBuffer sb = new StringBuffer(100);
         int lsp = resMessage.indexOf('\n');
         sb.append("<HTML><HEAD>").append(
                 "<TITLE>" + resCode + " " + (lsp < 0 ? resMessage : resMessage.substring(0, lsp)) + "</TITLE>")
                 .append("</HEAD><BODY " + BGCOLOR).append(
                 "><H2>" + resCode + " " + (lsp < 0 ? resMessage : resMessage.substring(0, lsp)) + "</H2>");
         if (lsp > 0)
            sb.append("<PRE>").append(Utils.htmlEncode(resMessage.substring(lsp), false)).append("</PRE>");
         sb.append("<HR>");
         sb.append(Identification.serverIdHtml);
         sb.append("</BODY></HTML>");
         setContentLength(sb.length());
         out.print(sb.toString());
         closeStreams();
      }

      // / Sends a redirect message to the client using the specified redirect
      // location URL.
      // @param location the redirect location URL
      // @exception IOException if an I/O error has occurred
      public void sendRedirect(String location) throws IOException
      {
         if (isCommitted())
            throw new IllegalStateException("Can not redirect, headers have been already written");
         if (location.indexOf(":/") < 0)
         { // relative
            String portString = "";
            if ("https".equalsIgnoreCase(getScheme()))
            {
               if (getServerPort() != 443)
                  portString = ":" + getServerPort();
            }
            else if (getServerPort() != 80)
               portString = ":" + getServerPort();

            if (location.length() > 0 && location.charAt(0) == '/')
            {
               location = getScheme() + "://" + getServerName() + portString + location;
            }
            else
            {
               int sp = reqUriPathUn.lastIndexOf('/');
               String uri;
               if (sp < 0)
               {
                  uri = reqUriPathUn + '/';
                  sp = uri.length();
               }
               else
               {
                  uri = reqUriPathUn;
                  sp++;
               }
               location = getScheme() + "://" + getServerName() + portString + uri.substring(0, sp) + location;
            }
         }
         // serve.log("location:"+location);
         setHeader("Location", location);
         setStatus(SC_MOVED_TEMPORARILY);
         setContentType("text/html");
         StringBuffer sb = new StringBuffer(200);
         sb.append("<HTML><HEAD>" + "<TITLE>" + SC_MOVED_TEMPORARILY + " Moved</TITLE>" + "</HEAD><BODY " + BGCOLOR
                 + "><H2>" + SC_MOVED_TEMPORARILY + " Moved</H2>" + "This document has moved <a href=" + location
                 + ">here.<HR>");
         sb.append(Identification.serverIdHtml);
         sb.append("</BODY></HTML>");
         setContentLength(sb.length());
         // to avoid further out
         out.print(sb.toString());
         closeStreams();
      }

      // URL session-encoding stuff. Not implemented, but the API is here
      // for compatibility.

      // / Encodes the specified URL by including the session ID in it, or, if
      // encoding is not needed, returns the URL unchanged. The
      // implementation of this method should include the logic to determine
      // whether the session ID needs to be encoded in the URL. For example,
      // if the browser supports cookies, or session tracking is turned off,
      // URL encoding is unnecessary.
      // <P>
      // All URLs emitted by a Servlet should be run through this method.
      // Otherwise, URL rewriting cannot be used with browsers which do not
      // support cookies.
      // @deprecated

      public String encodeUrl(String url)
      {
         return encodeURL(url);
      }

      // / Encodes the specified URL for use in the sendRedirect method or, if
      // encoding is not needed, returns the URL unchanged. The
      // implementation of this method should include the logic to determine
      // whether the session ID needs to be encoded in the URL. Because the
      // rules for making this determination differ from those used to
      // decide whether to encode a normal link, this method is seperate
      // from the encodeUrl method.
      // <P>
      // All URLs sent to the HttpServletResponse.sendRedirect method should
      // be
      // run through this method. Otherwise, URL rewriting cannot be used with
      // browsers which do not support cookies.
      public String encodeRedirectUrl(String url)
      {
         return encodeRedirectURL(url);
      }

      public Socket getSocket()
      {
         // TODO apply security check
         return socket;
      }
   }

   protected static class BasicAuthRealm extends Hashtable
   {
      String name;

      BasicAuthRealm(String name)
      {
         this.name = name;
      }

      String name()
      {
         return name;
      }
   }

   public static class ServeInputStream extends ServletInputStream
   {
      private final static boolean STREAM_DEBUG = false;

      /**
       * The actual input stream (buffered).
       */
      private InputStream in, origIn;

      private ServeConnection conn;

      private int chunksize = 0;

      private boolean chunking = false, compressed;

      private boolean returnedAsReader, returnedAsStream;

      private long contentLength = -1;

      private long readCount;

      private byte[] oneReadBuf = new byte[1];

      private boolean closed;

      /* ------------------------------------------------------------ */
      /**
       * Constructor
       */
      public ServeInputStream(InputStream in, ServeConnection conn)
      {
         this.conn = conn;
         this.in = new BufferedInputStream(in);
      }

      void refresh()
      {
         returnedAsReader = false;
         returnedAsStream = false;
         contentLength = -1;
         readCount = 0;
         chunksize = 0;
         closed = false;
         compressed(false);
      }

      /* ------------------------------------------------------------ */
      /**
       * @param chunking
       */
      public void chunking(boolean chunking)
      {
         if (contentLength == -1)
            this.chunking = chunking;
      }

      boolean compressed(boolean on)
      {
         if (on)
         {
            if (compressed == false)
            {
               origIn = in;
               try
               {
                  ServeInputStream sis = new ServeInputStream(in, conn);
                  if (chunking)
                  {
                     sis.chunking(true);
                     chunking(false);
                  }
                  in = (InputStream) conn.serve.gzipInStreamConstr.newInstance(new Object[]{sis});
                  compressed = true;
                  //conn.serve.log("Compressed stream was created with success", null);
               }
               catch (Exception ex)
               {
                  if (ex instanceof InvocationTargetException)
                     conn.serve.log("Problem in compressed stream creation", ((InvocationTargetException) ex).getTargetException());
                  else
                     conn.serve.log("Problem in compressed stream obtaining", ex);
               }
            }
         }
         else if (compressed)
         {
            compressed = false;
            in = origIn;
         }
         return compressed;
      }

      /**
       * sets max read byte in input
       */
      void setContentLength(long contentLength)
      {
         if (this.contentLength == -1 && contentLength >= 0 && chunking == false)
         {
            this.contentLength = contentLength;
            readCount = 0;
         }
      }

      /* ------------------------------------------------------------ */
      /**
       * Read a line ended by CRLF, used internally only for reading headers.
       * No char encoding, ASCII only
       */
      protected String readLine(int maxLen) throws IOException
      {
         if (maxLen <= 0)
            throw new IllegalArgumentException("Max len:" + maxLen);
         StringBuffer buf = new StringBuffer(Math.min(1024, maxLen));

         int c;
         boolean cr = false;
         int i = 0;
         while ((c = in.read()) != -1)
         {
            if (c == 10)
            { // LF
               if (cr)
                  break;
               break;
               //throw new IOException ("LF without CR");
            }
            else if (c == 13) // CR
               cr = true;
            else
            {
               //if (cr)
               //throw new IOException ("CR without LF");
               // see http://www.w3.org/Protocols/HTTP/1.1/rfc2616bis/draft-lafon-rfc2616bis-03.html#tolerant.applications
               cr = false;
               if (i >= maxLen)
                  throw new IOException("Line lenght exceeds " + maxLen);
               buf.append((char) c);
               i++;
            }
         }
         if (STREAM_DEBUG)
            System.err.println(buf);
         if (c == -1 && buf.length() == 0)
            return null;

         return buf.toString();
      }

      /* ------------------------------------------------------------ */
      public int read() throws IOException
      {
         int result = read(oneReadBuf, 0, 1);
         if (result == 1)
            return 255 & oneReadBuf[0];
         return -1;
      }

      /* ------------------------------------------------------------ */
      public int read(byte b[]) throws IOException
      {
         return read(b, 0, b.length);
      }

      /* ------------------------------------------------------------ */
      public synchronized int read(byte b[], int off, int len) throws IOException
      {
         if (closed)
            throw new IOException("The stream is already closed");
         if (chunking)
         {
            if (chunksize <= 0 && getChunkSize() <= 0)
               return -1;
            if (len > chunksize)
               len = chunksize;
            len = in.read(b, off, len);
            chunksize = (len < 0) ? -1 : (chunksize - len);
         }
         else
         {
            if (contentLength >= 0)
            {
               if (contentLength - readCount < Integer.MAX_VALUE)

                  len = Math.min(len, (int) (contentLength - readCount));
               if (len <= 0)
               {
                  if (STREAM_DEBUG)
                     System.err.print("EOF");
                  return -1;
               }
               len = in.read(b, off, len);
               if (len > 0)
                  readCount += len;
            }
            else
               // to avoid extra if
               len = in.read(b, off, len);
         }
         if (STREAM_DEBUG && len > 0)
            System.err.print(new String(b, off, len));

         return len;
      }

      /* ------------------------------------------------------------ */
      public long skip(long len) throws IOException
      {
         if (STREAM_DEBUG)
            System.err.println("instream.skip() :" + len);
         if (closed)
            throw new IOException("The stream is already closed");
         if (chunking)
         {
            if (chunksize <= 0 && getChunkSize() <= 0)
               return -1;
            if (len > chunksize)
               len = chunksize;
            len = in.skip(len);
            chunksize = (len < 0) ? -1 : (chunksize - (int) len);
         }
         else
         {
            if (contentLength >= 0)
            {
               len = Math.min(len, contentLength - readCount);
               if (len <= 0)
                  return -1;
               len = in.skip(len);
               readCount += len;
            }
            else
               len = in.skip(len);
         }
         return len;
      }

      /* ------------------------------------------------------------ */
      /**
       * Available bytes to read without blocking. If you are unlucky may return 0 when there are more
       */
      public int available() throws IOException
      {
         if (STREAM_DEBUG)
            System.err.println("instream.available()");
         if (closed)
            throw new IOException("The stream is already closed");
         if (chunking)
         {
            int len = in.available();
            if (len <= chunksize)
               return len;
            return chunksize;
         }

         if (contentLength >= 0)
         {
            int len = in.available();
            if (contentLength - readCount < Integer.MAX_VALUE)
               return Math.min(len, (int) (contentLength - readCount));
            return len;
         }
         else
            return in.available();
      }

      /* ------------------------------------------------------------ */
      public void close() throws IOException
      {
         // keep alive, will be closed by socket
         // in.close();
         if (STREAM_DEBUG)
            System.err.println("instream.close() " + closed);
         if (closed)
            return; //throw new IOException("The stream is already closed");
         // read until end of chunks or content length
         if (chunking)
            while (read() >= 0) ;
         else if (contentLength < 0) ;
         else
         {
            long skipCount = contentLength - readCount;
            while (skipCount > 0)
            {
               long skipped = skip(skipCount);
               if (skipped <= 0)
                  break;
               skipCount -= skipped;
            }
         }
         if (conn.keepAlive == false)
            in.close();
         closed = true;
      }

      /* ------------------------------------------------------------ */
      /**
       * Mark is not supported
       *
       * @return false
       */
      public boolean markSupported()
      {
         return false;
      }

      /* ------------------------------------------------------------ */
      /**
       *
       */
      public void reset() throws IOException
      {
         // no buffering, so not possible
         if (closed)
            throw new IOException("The stream is already closed");
         if (STREAM_DEBUG)
            System.err.println("instream.reset()");
         in.reset();
      }

      /* ------------------------------------------------------------ */
      /**
       * Not Implemented
       *
       * @param readlimit
       */
      public void mark(int readlimit)
      {
         // not supported
         if (STREAM_DEBUG)
            System.err.println("instream.mark(" + readlimit + ")");
      }

      /* ------------------------------------------------------------ */
      private int getChunkSize() throws IOException
      {
         if (chunksize < 0)
            return -1;

         chunksize = -1;

         // Get next non blank line
         chunking = false;
         String line = readLine(60);
         while (line != null && line.length() == 0)
            line = readLine(60);
         chunking = true;

         // Handle early EOF or error in format
         if (line == null)
            return -1;

         // Get chunksize
         int i = line.indexOf(';');
         if (i > 0)
            line = line.substring(0, i).trim();
         try
         {
            chunksize = Integer.parseInt(line, 16);
         }
         catch (NumberFormatException nfe)
         {
            throw new IOException("Chunked stream is broken, " + line);
         }

         // check for EOF
         if (chunksize == 0)
         {
            chunksize = -1;
            // Look for footers
            readLine(60);
            chunking = false;
         }
         return chunksize;
      }

      boolean isReturnedAsStream()
      {
         return returnedAsStream;
      }

      void setReturnedAsStream(boolean _on)
      {
         returnedAsStream = _on;
      }

      boolean isReturnedAsReader()
      {
         return returnedAsReader;
      }

      void setReturnedAsReader(boolean _on)
      {
         returnedAsReader = _on;
      }
   }

   public static class ServeOutputStream extends ServletOutputStream
   {

      private static final boolean STREAM_DEBUG = false;

      private boolean chunked;

      private boolean closed;

      // TODO: predefine as static byte[] used by chunked
      // underneath stream
      private OutputStream out;

      // private BufferedWriter writer; // for top speed
      private ServeConnection conn;

      private int inInclude;

      private String encoding;

      private/*volatile*/ long lbytes;

      private Utils.SimpleBuffer buffer;

      public ServeOutputStream(OutputStream out, ServeConnection conn)
      {
         this.out = out;
         this.conn = conn;
         buffer = new Utils.SimpleBuffer();
         encoding = conn.getCharacterEncoding();
         if (encoding == null)
            encoding = Utils.ISO_8859_1;
      }

      void refresh()
      {
         chunked = false;
         closed = false;
         inInclude = 0;
         lbytes = 0;
         buffer.reset();
         encoding = conn.getCharacterEncoding();
         if (encoding == null)
            encoding = Utils.ISO_8859_1;
      }

      protected void reset()
      {
         if (lbytes == 0)
            buffer.reset();
         else
            throw new IllegalStateException("Result was already committed");
      }

      protected int getBufferSize()
      {
         return buffer.getSize();
      }

      protected void setBufferSize(int size)
      {
         if (lbytes > 0)
            throw new IllegalStateException("Bytes already written in response");
         buffer.setSize(size);
      }

      protected void setChunked(boolean set)
      {
         chunked = set;
      }

      public void print(String s) throws IOException
      {
         write(s.getBytes(encoding));
      }

      public void write(int b) throws IOException
      {
         write(new byte[]{(byte) b}, 0, 1);
      }

      public void write(byte[] b) throws IOException
      {
         write(b, 0, b.length);
      }

      public void write(byte[] b, int off, int len) throws IOException
      {
         if (closed)
         {
            if (STREAM_DEBUG)
               System.err.println((b == null ? "null" : new String(b, off, len)) + "\n won't be written, stream closed.");
            throw new IOException("An attempt of writing " + len + " bytes to a closed out.");
         }

         if (len == 0)
            return;
         //
         conn.writeHeaders();
         b = buffer.put(b, off, len);
         len = b.length;
         if (len == 0)
            return;
         off = 0;
         if (chunked)
         {
            String hexl = Integer.toHexString(len);
            out.write((hexl + "\r\n").getBytes()); // no encoding Ok
            lbytes += 2 + hexl.length();
            out.write(b, off, len);
            lbytes += len;
            out.write("\r\n".getBytes());
            lbytes += 2;
         }
         else
         {
            out.write(b, off, len);
            lbytes += len;
         }

         if (STREAM_DEBUG)
         {
            if (chunked)
               System.err.println(Integer.toHexString(len));
            System.err.print(new String(b, off, len));
            if (chunked)
               System.err.println();
         }
      }

      public void flush() throws IOException
      {
         if (closed)
            return;
         // throw new IOException("An attempt of flushig closed out.");
         conn.writeHeaders();
         byte[] b = buffer.get();
         if (b.length > 0)
         {
            if (chunked)
            {
               String hexl = Integer.toHexString(b.length);
               out.write((hexl + "\r\n").getBytes()); // no encoding Ok
               lbytes += 2 + hexl.length();
               out.write(b);
               lbytes += b.length;
               out.write("\r\n".getBytes());
               lbytes += 2;
               if (STREAM_DEBUG)
               {
                  System.err.println(hexl);
                  System.err.print(new String(b));
                  System.err.println();
               }
            }
            else
            {
               out.write(b);
               lbytes += b.length;
               if (STREAM_DEBUG)
               {
                  System.err.print(new String(b));
               }
            }
         }
         out.flush();
      }

      public void close() throws IOException
      {
         if (closed)
            return;
         // throw new IOException("Stream is already closed.");
         // new IOException("Stream closing").printStackTrace();
         try
         {
            flush();
            if (inInclude == 0)
            {
               if (chunked)
               {
                  out.write("0\r\n\r\n".getBytes());
                  lbytes += 5;
                  if (STREAM_DEBUG)
                     System.err.print("0\r\n\r\n");
                  // TODO: here is possible to write trailer headers
                  out.flush();
               }
               if (conn.keepAlive == false)
                  out.close();
            }
         }
         finally
         {
            closed = true;
         }
      }

      private long lengthWritten()
      {
         return lbytes;
      }

      boolean isInInclude()
      {
         return inInclude == 0;
      }

      void setInInclude(boolean _set)
      {
         inInclude = _set ? 1 : 0;
         /*if (_set)
               inInclude++;
            else
               inInclude--;
            if (inInclude < 0)
               throw new IllegalStateException("Not matching include set");*/
      }
   }

   /**
    * Class PathTreeDictionary - this class allows to put path elements in format n1/n2/n2[/*.ext] and get match to a pattern and a unmatched tail
    */
   public static class PathTreeDictionary
   {
      Node root_node;

      public PathTreeDictionary()
      {
         root_node = new Node();
      }

      public synchronized void put(String path, Object value)
      {
         StringTokenizer st = new StringTokenizer(path, "\\/");
         Node cur_node = root_node;
         while (st.hasMoreTokens())
         {
            String nodename = st.nextToken();
            Node node = (Node) cur_node.get(nodename);
            if (node == null)
            {
               node = new Node();
               cur_node.put(nodename, node);
            }
            cur_node = node;
         }
         cur_node.object = value;
      }

      public synchronized Object[] remove(Object value)
      {
         return remove(root_node, value);
      }

      public synchronized Object[] remove(String path)
      {
         Object[] result = get(path);
         if (result[1] != null)
            return remove(result[1]);
         return result;
      }

      public Object[] remove(Node node, Object value)
      {
         // TODO make full path, not only last element
         Enumeration e = node.keys();
         while (e.hasMoreElements())
         {
            String path = (String) e.nextElement();
            Node childNode = (Node) node.get(path);
            if (childNode.object == value)
            {// it's safe because the same instance can't be shared for several paths in this design
               childNode.object = null;
               return new Object[]{value, new Integer(0)};
            }
            Object[] result = remove(childNode, value);
            if (result[0] != null)
               return result;
         }

         return new Object[]{null, null};
      }

      /**
       * This function looks up in the directory to find the perfect match and remove matching part from path, so if you need to keep original path, save it
       * somewhere
       */
      public Object[] get(String path)
      {
         Object[] result = new Object[2];
         if (path == null)
            return result;
         char[] ps = path.toCharArray();
         Node cur_node = root_node;
         int p0 = 0, lm = 0; // last match
         result[0] = cur_node.object;
         boolean div_state = true;
         for (int i = 0; i < ps.length; i++)
         {
            if (ps[i] == '/' || ps[i] == '\\')
            {
               if (div_state)
                  continue;
               Node node = (Node) cur_node.get(new String(ps, p0, i - p0));
               if (node == null)
               {
                  result[1] = new Integer(lm);
                  return result;
               }
               if (node.object != null)
               {
                  result[0] = node.object;
                  lm = i;
               }
               cur_node = node;
               div_state = true;
            }
            else
            {
               if (div_state)
               {
                  p0 = i;
                  div_state = false;
               }
            }
         }
         cur_node = (Node) cur_node.get(new String(ps, p0, ps.length - p0));
         if (cur_node != null && cur_node.object != null)
         {
            result[0] = cur_node.object;
            lm = ps.length;
         }
         result[1] = new Integer(lm);
         return result;
      }

      public Enumeration keys()
      {
         Vector result = new Vector();
         addSiblingNames(root_node, result, "");
         return result.elements();
      }

      public void addSiblingNames(Node node, Vector result, String path)
      {
         Enumeration e = node.keys();
         while (e.hasMoreElements())
         {
            String pc = (String) e.nextElement();
            Node childNode = (Node) node.get(pc);
            pc = path + '/' + pc;
            if (childNode.object != null)
               result.addElement(pc);
            addSiblingNames(childNode, result, pc);
         }
      }

      public Enumeration elements()
      {
         Vector result = new Vector();
         addSiblingObjects(root_node, result);
         return result.elements();
      }

      public void addSiblingObjects(Node node, Vector result)
      {
         Enumeration e = node.keys();
         while (e.hasMoreElements())
         {
            Node childNode = (Node) node.get(e.nextElement());
            if (childNode.object != null)
               result.addElement(childNode.object);
            addSiblingObjects(childNode, result);
         }
      }

      class Node extends Hashtable
      {
         Object object;
      }
   }

   /**
    * Http session support
    * <p/>
    * TODO: provide lazy session restoring, it should allow to load classes from wars 1st step it read serialization data and store under session attribute 2nd
    * when the session requested, it tries to deserialize all session attributes considered that all classes available
    */
   public static class AcmeSession extends Hashtable implements HttpSession
   {
      private long createTime;

      private long lastAccessTime;

      private String id;

      private int inactiveInterval; // in seconds

      private boolean expired;

      private transient ServletContext servletContext;

      private transient HttpSessionContext sessionContext;

      private transient List listeners;

      // TODO: check in documentation what is default inactive interval and
      // what
      // means 0
      // and what is mesurement unit
      AcmeSession(String id, ServletContext servletContext, HttpSessionContext sessionContext)
      {
         this(id, 0, servletContext, sessionContext);
      }

      AcmeSession(String id, int inactiveInterval, ServletContext servletContext, HttpSessionContext sessionContext)
      {
         // new Exception("Session created with: "+servletContext).printStackTrace(); //!!!
         createTime = System.currentTimeMillis();
         this.id = id;
         this.inactiveInterval = inactiveInterval;
         this.servletContext = servletContext;
         this.sessionContext = sessionContext;
      }

      public long getCreationTime()
      {
         return createTime;
      }

      public String getId()
      {
         return id;
      }

      public long getLastAccessedTime()
      {
         return lastAccessTime;
      }

      public void setMaxInactiveInterval(int interval)
      {
         inactiveInterval = interval;
      }

      public int getMaxInactiveInterval()
      {
         return inactiveInterval;
      }

      /**
       * @deprecated
       */
      public HttpSessionContext getSessionContext()
      {
         return sessionContext;
      }

      /**
       * Returns the ServletContext to which this session belongs.
       *
       * @return The ServletContext object for the web application
       * @ince 2.3
       */
      public ServletContext getServletContext()
      {
         // System.err.println("ctx from:"+servletContext); //!!!
         return servletContext;
      }

      public java.lang.Object getAttribute(java.lang.String name) throws IllegalStateException
      {
         if (expired)
            throw new IllegalStateException();
         return get((Object) name);
      }

      public java.lang.Object getValue(java.lang.String name) throws IllegalStateException
      {
         return getAttribute(name);
      }

      public java.util.Enumeration getAttributeNames() throws IllegalStateException
      {
         if (expired)
            throw new IllegalStateException();
         return keys();
      }

      public java.lang.String[] getValueNames() throws IllegalStateException
      {
         Enumeration e = getAttributeNames();
         Vector names = new Vector();
         while (e.hasMoreElements())
            names.addElement(e.nextElement());
         String[] result = new String[names.size()];
         names.copyInto(result);
         return result;
      }

      public void setAttribute(String name, Object value) throws IllegalStateException
      {
         if (expired)
            throw new IllegalStateException();
         Object oldValue = value != null ? put((Object) name, value) : remove(name);
         if (oldValue != null)
            if (oldValue instanceof HttpSessionBindingListener)
               ((HttpSessionBindingListener) oldValue).valueUnbound(new HttpSessionBindingEvent(this, name));
            else if (oldValue instanceof HttpSessionAttributeListener)
               ((HttpSessionAttributeListener) oldValue).attributeReplaced(new HttpSessionBindingEvent(this, name,
                       value));
         if (value instanceof HttpSessionBindingListener)
            ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name));
         else if (value instanceof HttpSessionAttributeListener)
            ((HttpSessionAttributeListener) value).attributeAdded(new HttpSessionBindingEvent(this, name));
      }

      public void putValue(String name, Object value) throws IllegalStateException
      {
         setAttribute(name, value);
      }

      public void removeAttribute(java.lang.String name) throws IllegalStateException
      {
         if (expired)
            throw new IllegalStateException();
         Object value = remove((Object) name);
         if (value != null)
            if (value instanceof HttpSessionBindingListener)
               ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name));
            else if (value instanceof HttpSessionAttributeListener)
               ((HttpSessionAttributeListener) value).attributeRemoved(new HttpSessionBindingEvent(this, name));
      }

      public void removeValue(java.lang.String name) throws IllegalStateException
      {
         removeAttribute(name);
      }

      public synchronized void invalidate() throws IllegalStateException
      {
         if (expired)
            throw new IllegalStateException();
         notifyListeners();
         Enumeration e = getAttributeNames();
         while (e.hasMoreElements())
         {
            removeAttribute((String) e.nextElement());
         }
         setExpired(true);
         // would be nice remove it from hash table also
      }

      public boolean isNew() throws IllegalStateException
      {
         if (expired)
            throw new IllegalStateException();
         return lastAccessTime == 0;
      }

      public synchronized void setListeners(List l)
      {
         if (listeners == null)
         {
            listeners = l;
            if (listeners != null)
            {
               HttpSessionEvent event = new HttpSessionEvent(this);
               for (int i = 0; i < listeners.size(); i++)
                  try
                  {
                     ((HttpSessionListener) listeners.get(0)).sessionCreated(event);
                  }
                  catch (ClassCastException cce)
                  {
                     // log("Wrong session listener type."+cce);
                  }
                  catch (NullPointerException npe)
                  {
                     // log("Null session listener.");
                  }
            }
         }
      }

      /**
       * something hack, to update servlet context since session created out of scope
       *
       * @param sc
       */
      public synchronized void setServletContext(ServletContext sc)
      {
         // System.err.println("ctx to:"+servletContext); //!!!
         servletContext = sc;
      }

      private void notifyListeners()
      {
         if (listeners != null)
         {
            HttpSessionEvent event = new HttpSessionEvent(this);
            for (int i = 0; i < listeners.size(); i++)
               try
               {
                  ((HttpSessionListener) listeners.get(i)).sessionDestroyed(event);
               }
               catch (ClassCastException cce)
               {
                  // log("Wrong session listener type."+cce);
               }
               catch (NullPointerException npe)
               {
                  // log("Null session listener.");
               }
         }
      }

      private void setExpired(boolean expired)
      {
         this.expired = expired;
      }

      boolean isValid()
      {
         return !expired;
      }

      boolean checkExpired()
      {
         return inactiveInterval > 0 && (inactiveInterval * 1000 < System.currentTimeMillis() - lastAccessTime);
      }

      void userTouch()
      {
         if (isValid())
            lastAccessTime = System.currentTimeMillis();
         else
            throw new IllegalStateException();
      }

      // storing session in format
      // id:latency:contextname:tttt
      // entry:base64 ser data
      // entry:base64 ser data
      // $$
      void save(Writer w) throws IOException
      {
         if (expired)
            return;
         // can't use append because old JDK
         w.write(id);
         w.write(':');
         w.write(Integer.toString(inactiveInterval));
         w.write(':');
         w.write(servletContext == null || servletContext.getServletContextName() == null ? "" : servletContext
                 .getServletContextName());
         w.write(':');
         w.write(Long.toString(lastAccessTime));
         w.write("\r\n");
         Enumeration e = getAttributeNames();
         ByteArrayOutputStream os = new ByteArrayOutputStream(1024 * 16);
         while (e.hasMoreElements())
         {
            String aname = (String) e.nextElement();
            Object so = get(aname);
            if (so instanceof Serializable)
            {
               os.reset();
               ObjectOutputStream oos = new ObjectOutputStream(os);
               try
               {
                  oos.writeObject(so);
                  w.write(aname);
                  w.write(":");
                  w.write(Utils.base64Encode(os.toByteArray()));
                  w.write("\r\n");
               }
               catch (IOException ioe)
               {
                  servletContext.log("Problem storing a session value of " + aname, ioe);
               }
            }
            else
               servletContext.log("Non serializable object " + so.getClass().getName() + " skiped in storing of " + aname, null);
            if (so instanceof HttpSessionActivationListener)
               ((HttpSessionActivationListener) so).sessionWillPassivate(new HttpSessionEvent(this));
         }
         w.write("$$\r\n");
      }

      static AcmeSession restore(BufferedReader r, int inactiveInterval, ServletContext servletContext,
                                 HttpSessionContext sessionContext) throws IOException
      {
         String s = r.readLine();
         if (s == null) // eos
            return null;
         int cp = s.indexOf(':');
         if (cp < 0)
            throw new IOException("Invalid format for a session header, no session id: " + s);
         String id = s.substring(0, cp);
         int cp2 = s.indexOf(':', cp + 1);
         if (cp2 < 0)
            throw new IOException("Invalid format for a session header, no latency: " + s);
         try
         {
            inactiveInterval = Integer.parseInt(s.substring(cp + 1, cp2));
         }
         catch (NumberFormatException nfe)
         {
            servletContext.log("Session latency is invalid:" + s.substring(cp + 1, cp2) + " " + nfe);
         }
         cp = s.indexOf(':', cp2 + 1);
         if (cp < 0)
            throw new IOException("Invalid format for a session header, context name: " + s);
         String contextName = s.substring(cp2 + 1, cp);
         // consider servletContext.getContext("/"+contextName)
         AcmeSession result = new AcmeSession(id, inactiveInterval, contextName.length() == 0 ? servletContext
                 : null, sessionContext);
         try
         {
            result.lastAccessTime = Long.parseLong(s.substring(cp + 1));
         }
         catch (NumberFormatException nfe)
         {
            servletContext.log("Last access time is invalid:" + s.substring(cp + 1) + " " + nfe);
         }
         do
         {
            s = r.readLine();
            if (s == null)
               throw new IOException("Unexpected end of a stream.");
            if ("$$".equals(s))
               return result;
            cp = s.indexOf(':');
            if (cp < 0)
               throw new IOException("Invalid format for a session entry: " + s);
            String aname = s.substring(0, cp);
            // if (lazyRestore)
            // result.put(aname, s.substring(cp+1));
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Utils.decode64(s
                    .substring(cp + 1))));
            Throwable restoreError;
            try
            {
               Object so;
               result.put(aname, so = ois.readObject());
               restoreError = null;
               if (so instanceof HttpSessionActivationListener)
                  ((HttpSessionActivationListener) so).sessionDidActivate(new HttpSessionEvent(result));

            }
            catch (ClassNotFoundException cnfe)
            {
               restoreError = cnfe;

            }
            catch (NoClassDefFoundError ncdfe)
            {
               restoreError = ncdfe;
            }
            catch (IOException ioe)
            {
               restoreError = ioe;
            }
            if (restoreError != null)
               servletContext.log("Can't restore :" + aname + ", " + restoreError);
         } while (true);
      }
   }

   protected static class LocaleWithWeight implements Comparable
   {
      protected float weight; // should be int

      protected Locale locale;

      LocaleWithWeight(Locale l, float w)
      {
         locale = l;
         weight = w;
         // System.err.println("Created "+l+", with:"+w);
      }

      public int compareTo(Object o)
      {
         if (o instanceof LocaleWithWeight)
            return (int) (((LocaleWithWeight) o).weight - weight) * 100;
         throw new IllegalArgumentException();
      }

      public Locale getLocale()
      {
         return locale;
      }
   }

   protected static class AcceptLocaleEnumeration implements Enumeration
   {
      Iterator i;

      public AcceptLocaleEnumeration(TreeSet/* <LocaleWithWeight> */ts)
      {
         i = ts.iterator();
      }

      public boolean hasMoreElements()
      {
         return i.hasNext();
      }

      public Object nextElement()
      {
         return ((LocaleWithWeight) i.next()).getLocale();
         /*
             * Locale l =((LocaleWithWeight)i.next()).getLocale(); System.err.println("Returned l:"+l); return l;
             */
      }
   }

   // TODO: reconsider implementation by providing
   // inner class implementing HttpSessionContext
   // and returning it on request
   // to avoid casting this class to Hashtable

   protected static class HttpSessionContextImpl extends Hashtable implements HttpSessionContext
   {

      public java.util.Enumeration getIds()
      {
         return keys();
      }

      public HttpSession getSession(java.lang.String sessionId)
      {
         return (HttpSession) get(sessionId);
      }

      void save(Writer w) throws IOException
      {
         Enumeration e = elements();
         while (e.hasMoreElements())
            ((AcmeSession) e.nextElement()).save(w);
      }

      static HttpSessionContextImpl restore(BufferedReader br, int inactiveInterval, ServletContext servletContext)
              throws IOException
      {
         HttpSessionContextImpl result = new HttpSessionContextImpl();
         AcmeSession session;
         while ((session = AcmeSession.restore(br, inactiveInterval, servletContext, result)) != null)
            if (session.checkExpired() == false)
               result.put(session.getId(), session);
         return result;
      }
   }
}