Package Acme.Serve

Source Code of Acme.Serve.Serve$HttpSessionContextImpl

// 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-2011 by Dmitriy Rogatkin
// This version is compatible with JSDK 2.5
// http://tjws.sourceforge.net
// $Id: Serve.java,v 1.226 2011/07/22 05:46:53 dmitriy Exp $

package Acme.Serve;

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.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.net.SocketTimeoutException;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.LinkedList;
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.lang.reflect.Method;

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 Acme.Utils;

/// 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 ARG_PROXY_CONFIG = "proxy-config";

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

  public static final String DEF_PROXY_CONFIG = "tjws.proxy.ssl";

  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 boolean proxyConfig;

  private boolean proxySSL;

  private int timeoutKeepAlive;

  private int maxAliveConnUse;

  private boolean showUserAgent;

  private boolean showReferer;

  protected String keepAliveHdrParams;

  protected transient PathTreeDictionary defaultRegistry;

  protected transient HashMap virtuals;

  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;

  private static final ThreadLocal currentRegistry = new ThreadLocal();

  // 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;

  // / Constructor.
  public Serve(Map arguments, PrintStream logStream) {
    this.arguments = arguments;
    this.logStream = logStream;
    defaultRegistry = 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();
    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) {

    }
    String proxyArg = (String) arguments.get(ARG_PROXY_CONFIG);
    if (proxyArg != null) {
      proxyConfig = true;
      proxySSL = "y".equalsIgnoreCase(proxyArg);
    }
    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;
    } else
      useAccLog = false;
  }

  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("TJWS: 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, null);
    } catch (ClassNotFoundException e) {
      log("TJWS: Class not found: " + className);
      ClassLoader cl = getClass().getClassLoader();
      log("TJWS: Class loader: " + cl);
      if (cl instanceof java.net.URLClassLoader)
        log("TJWS: CP: "
            + java.util.Arrays
                .asList(((java.net.URLClassLoader) cl)
                    .getURLs()));
    } catch (ClassCastException e) {
      log("TJWS: Servlet class doesn't implement javax.servlet.Servlet: "
          + e.getMessage());
    } catch (InstantiationException e) {
      log("TJWS: Can't instantiate servlet: " + e.getMessage());
    } catch (IllegalAccessException e) {
      log("TJWS: Illegal class access: " + e.getMessage());
    } catch (Exception e) {
      log("TJWS: 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
   * @param host
   *            name the servlet attached to, when verual hosts are used, use
   *            null for all hosts
   */
  public void addServlet(String urlPat, Servlet servlet, String hostName) {
    addServlet(urlPat, servlet, (Hashtable) null, hostName);
  }

  /**
   * 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, (String) null);
  }

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

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

  public void unloadSessions(ServletContext servletContext) {
    if (sessions == null)
      return;
    File store = getPersistentFile(servletContext);

    Writer w = null;
    try {
      if (store != null) {
        w = new FileWriter(store);
      }
      // TODO consider moving implementation to HttpSessionContextImpl
      Enumeration e = sessions.elements();
      while (e.hasMoreElements()) {
        AcmeSession session = (AcmeSession) e.nextElement();
        if (session.getServletContext() == servletContext) {
          if (w != null)
            try {
              session.save(w);
            } catch (Throwable t) {
              if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
              log("TJWS: problem in session serialization for "+servletContext+" / "+
                  session, t);
            }
          session.invalidate();
        }
      }
    } catch (IOException ioe) {
      log("TJWS: problem in persisting sessions for  "+servletContext, ioe);
    } finally {
      if (w != null)
        try {
          w.close();
        } catch (Exception e) {
        }
    }
  }

  public void restoreSessions(ServletContext servletContext) {
    File store = sessions == null?null:getPersistentFile(servletContext);
    if (store != null && store.exists()) {
      BufferedReader bfr = null;
      try {
        bfr = new BufferedReader(new FileReader(store));

        AcmeSession session;
        while ((session = AcmeSession.restore(bfr,
            Math.abs(expiredIn) * 60, servletContext, sessions)) != null)
          if (session.checkExpired() == false)
            sessions.put(session.getId(), session);
      } catch (IOException ioe) {
        log("TJWS: problem in sessions deserialization for "+servletContext, ioe);
      } finally {
        if (bfr != null)
          try {
            bfr.close();
          } catch (Exception e) {

          }
      }
    }
  }

  // / 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("TJWS: 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("TJWS: Servlet for path '/' already defined and no default will be used.");
  }

  // TODO review this method and figure throttles use
  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("TJWS: Problem initializing war deployer: " + cnf);
    } catch (Throwable t) {
      if (t instanceof ThreadDeath)
        throw (ThreadDeath) t;
      log("TJWS: Problem in war(s) deployment", t);
    }
  }

  protected File getPersistentFile() {
    return getPersistentFile(null);
  }

  protected File getPersistentFile(ServletContext servletContext) {
    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))
        + (servletContext == null ? ""
            : "-"+servletContext.getServletContextName())
        + "-session.obj");
  }

  protected void console(String msg) {
    System.out.println(msg);
  }

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

  protected transient Acceptor acceptor;

  protected transient Thread ssclThread;

  /**
   * 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() {
    if (running)
      return -2; // still running
    try {
      acceptor = createAcceptor();
    } catch (IOException e) {
      log("TJWS: Acceptor: " + e);
      return 1;
    }
    running = true;

    if (expiredIn > 0) {
      // sessions cleaner thread
      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;
    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("TJWS: IO error in restoring sessions.", ioe);
      } catch (Exception e) {
        log("TJWS: 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
    console("[" + new Date() + "] TJWS httpd " + hostName + " - "
        + acceptor + " is listening.");
    try {
      while (running) {
        try {
          Socket socket = acceptor.accept();
          // TODO consider to use ServeConnection object pool
          // TODO consider req/resp objects pooling
          keepAliveCleaner.addConnection(new ServeConnection(socket,
              this));
        } catch (IOException e) {
          log("TJWS: Accept: " + e);
        } catch (SecurityException se) {
          log("TJWS: Illegal access: " + se);
        } catch (IllegalStateException is) {
          log("TJWS: Illegal state: " + is);
        }
      }
    } catch (Throwable t) {
      if (t instanceof ThreadDeath)
        throw (Error) t;
      log("TJWS: Unhandled exception: " + t + ", server is terminating.",
          t);
      return -1;
    } finally {
      if (acceptor != null)
        try {
          acceptor.destroy();
          acceptor = null;
        } catch (IOException e) {
        }

      if (ssclThread != null)
        ssclThread.interrupt();
      keepAliveCleaner.interrupt();
      try {
        keepAliveCleaner.join();
      } catch (InterruptedException ie) {
      }
      keepAliveCleaner.clear(); // clear rest, although should be empty
    }
    return 0;
  }

  /**
   * Tells the server to stop
   *
   * @throws IOException
   */
  public void notifyStop() {
    running = false;
    try {
      acceptor.destroy();
      acceptor = null;
    } catch (IOException ioe) {

    } catch (NullPointerException npe) {
     
    }
  }

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

    public Socket accept() throws IOException;

    public void destroy() throws IOException;
  }

  public static interface AsyncCallback {
    public void notifyTimeout();

    public long getTimeout();
  }

  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("TJWS: 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("TJWS: Acceptor is not accessable or can't be instantiated, the Server is inoperable",
            e);
      }
    } catch (ClassNotFoundException e) {
      String fatal;
      log(fatal = "TJWS: Acceptor class not found, the Server is inoperable",
          e);
      console(fatal);
      System.exit(-2);
    }
    Map acceptorProperties = new Properties();
    acceptor.init(arguments, acceptorProperties);
    hostName = (String) acceptorProperties.get(ARG_BINDADDRESS);
    return acceptor;
  }

  public void setHost(String hostName) {
    if (virtuals == null)
      currentRegistry.set(defaultRegistry);
    else {
      if (hostName == null)
        currentRegistry.set(defaultRegistry);
      else {
        int colInd = hostName.indexOf(':'); // separate port part
        if (colInd > 0)
          hostName = hostName.substring(0, colInd);
        PathTreeDictionary registry = (PathTreeDictionary) virtuals
            .get(hostName.toLowerCase());
        if (registry != null)
          currentRegistry.set(registry);
        else
          currentRegistry.set(defaultRegistry);
      }
    }
  }

  // ///////////////// 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) {
    PathTreeDictionary registry = (PathTreeDictionary) currentRegistry
        .get();
    if (registry == null)
      registry = defaultRegistry;
    try {
      return (Servlet) registry.get(name)[0];
    } catch (NullPointerException npe) {
    }
    return null;
  }

  // / Enumerates the servlets in this context (server). Only servlets that
  // are accessible will be returned. This enumeration always includes the
  // servlet itself.
  public Enumeration getServlets() {
    PathTreeDictionary registry = (PathTreeDictionary) currentRegistry
        .get();
    if (registry == null)
      registry = defaultRegistry;
    return registry.elements();
  }

  // / Enumerates the names of the servlets in this context (server). Only
  // servlets that are accessible will be returned. This enumeration always
  // includes the servlet itself.
  public Enumeration getServletNames() {
    PathTreeDictionary registry = (PathTreeDictionary) currentRegistry
        .get();
    if (registry == null)
      registry = defaultRegistry;
    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("TJWS: Sessions stored.");
      } catch (IOException ioe) {
        log("TJWS: IO error in storing sessions " + ioe);
      } catch (Throwable t) {
        if (t instanceof ThreadDeath)
          throw (ThreadDeath) t;
        log("TJWS: Unexpected 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
    destroyAll(defaultRegistry);
    if (virtuals != null) {
      Iterator si = virtuals.values().iterator();
      while (si.hasNext())
        destroyAll((PathTreeDictionary) si.next());
    }

    // clean access tree
    defaultRegistry = new PathTreeDictionary();
    virtuals = null;
  }

  private void destroyAll(PathTreeDictionary registry) {
    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("TJWS: Destroying thread didn't terminate in "
            + DESTROY_TIME_SEC);
        destroyThread.setName("Destroying took too long " + (dhc++)); // let
                                        // it
                                        // running
                                        // with
                                        // different
                                        // name
        // destroyThread.stop();
      }
    }
  }

  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();
    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);
  }

  // / 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 + "]->[");
    path = Utils.canonicalizePath(path);
    if (path != null && 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>
   * 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>
   * The servlet container must implement the URL handlers and
   * <code>URLConnection</code> objects that are necessary to access the
   * resource.
   *
   * <p>
   * This method returns <code>null</code> if no resource is mapped to the
   * pathname.
   *
   * <p>
   * Some containers may allow writing to the URL returned by this method
   * using the methods of the URL class.
   *
   * <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>
   * 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
   *
   * @exception 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>
   * 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>
   * Meta-information such as content length and content type that is
   * available via <code>getResource</code> method is lost when using this
   * method.
   *
   * <p>
   * The servlet container must implement the URL handlers and
   * <code>URLConnection</code> objects necessary to access the resource.
   *
   * <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) {
      PathTreeDictionary registry = (PathTreeDictionary) currentRegistry
          .get();
      if (registry == null)
        registry = defaultRegistry;
      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 backward compatibility
  class KeepAliveCleaner extends Thread {
    protected List connections;

    protected List ingoings;

    protected boolean stopped;

    private boolean noCheckClose;

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

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

    void clear() {
      if (stopped == false)
        new IllegalStateException("Can't clear running cleaner");
      clear(ingoings);
      clear(connections);
    }

    private void clear(List ll) {
      Iterator i = ll.iterator();
      while (i.hasNext()) {
        ServeConnection conn = (ServeConnection) i.next();
        i.remove();
        conn.close();
      }
    }

    public void run() {
      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();
        long d = getKeepAliveDuration();
        // System.err.println("===> keep alive time"+d);
        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 (NoSuchMethodException e) {
                  noCheckClose = true;
                } catch (Exception e) {
                }
            }
          if (closed
              || conn.keepAlive == false
              || (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");
            }
            // System.err.println("===> Removing and closing con"+conn+" of "+conn.keepAlive);
          }
          if (conn.asyncTimeout > 0) {
            if (ct >= conn.asyncTimeout) {
              if (conn.asyncMode != null) {
                conn.asyncMode.notifyTimeout();
                conn.keepAlive = false;
                conn.joinAsync();
              }
            } else {
              long nd = conn.asyncTimeout - ct;
              if (nd < d)
                d = nd;
            }
          }
        }
        if (stopped && connections.size() == 0) // TODO stopped can be
                            // enough, since clear
                            // method
          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 with Android support";

    public static final String serverVersion = "Version 1.83, $Revision: 1.226 $";

    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 {
    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();

    private Socket socket;

    private Hashtable sslAttributes;

    private Serve serve;

    private ServletInputStream in;

    private ServletOutputStream out;

    private String scheme;

    private AsyncCallback asyncMode;

    private long asyncTimeout;

    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, reqSessionValue;

    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("-------"+this);
    // super.finalize();
    // }

    // / Constructor.
    public ServeConnection(Socket socket, Serve serve) {
      // serve.log("+++++++"+this);
      // Save arguments.
      this.socket = socket;
      this.serve = serve;
      expdatefmt.setTimeZone(tz);
      headerdateformat.setTimeZone(tz);
      rfc850DateFmt.setTimeZone(tz);
      asciiDateFmt.setTimeZone(tz);
      if (serve.isAccessLogged()) {
        // note format string must be not null
        accessFmt = new MessageFormat(
            (String) serve.arguments.get(ARG_ACCESS_LOG_FMT));
        logPlaceholders = new Object[12];
      }
      initSSLAttrs();
      try {
        in = new ServeInputStream(socket.getInputStream(), this);
        out = new ServeOutputStream(socket.getOutputStream(), this);
      } catch (IOException ioe) {
        close();
        return;
      }
      serve.threadPool.executeThread(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
     */
    private 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;
    }

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

    private void restart() throws IOException {
      // 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; // requested
      sessionUrlValue = null; // requested
      sessionValue = null; // actual used
      reqSessionValue = null; // requested and used
      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;
      if (asyncMode != null) {
        serve.log("TJWS: debug", new Exception(
            "Restarting without clean async mode"));
        asyncMode = null;
      }
      ((ServeInputStream) in).refresh();
      out = new ServeOutputStream(socket.getOutputStream(), this);
      // stream can be still used asyncronously, so
      // ((ServeOutputStream) out).refresh();
    }

    // Methods from Runnable.
    public void run() {
      if (socket == null)
        return;
      try {
        do {
          restart();
          // Get the streams.
          parseRequest();
          if (asyncMode != null) {
            asyncTimeout = asyncMode.getTimeout();
            if (asyncTimeout > 0)
              asyncTimeout += System.currentTimeMillis();
            return;
          }
          finalizerequest();
        } while (keepAlive && serve.isKeepAlive()
            && timesRequested < serve.getMaxTimesConnectionUse());
      } catch (IOException ioe) {
        // System.err.println("Drop "+ioe);
        if (ioe instanceof SocketTimeoutException) {
          // serve.log("Keepalive timeout, async " + asyncMode, null);
        } else {
          String errMsg = ioe.getMessage();
          if ((errMsg == null || errMsg.indexOf("ocket closed") < 0)
              && ioe instanceof java.nio.channels.AsynchronousCloseException == false)
            if (socket != null)
              serve.log("TJWS: IO error: " + ioe
                  + " in processing a request from "
                  + socket.getInetAddress() + ":"
                  + socket.getLocalPort() + " / "
                  + socket.getClass().getName()/* , ioe */);
            else
              serve.log("TJWS: IO error: " + ioe
                  + "(socket NULL)");
          else
            synchronized (this) {
              socket = null;
            }
        }
      } finally {
        if (asyncMode == null)
          close();
      }
    }

    synchronized final void close() {
      if (socket != null)
        try {
          socket.close();
          socket = null;
        } catch (IOException e) { /* ignore */
        }
    }

    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.
      socket.setSoTimeout(serve.timeoutKeepAlive);
      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;
      }
      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) {
            } catch (IllegalStateException ise) {
            }
          }
          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("TJWS: 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("TJWS: Invalid value of input content-length: "
              + contentLength);
        }
      // the code was originally in processing headers loop, however hhas
      // been moved here
      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);
      socket.setSoTimeout(0);
      serve.setHost(getHeader(HOST));
      PathTreeDictionary registry = (PathTreeDictionary) currentRegistry
          .get();
      try {
        // TODO new
        // SimpleRequestDispatcher(reqUriPathUn).forward((ServletRequest)
        // this, (ServletResponse) this);
        Object[] os = 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);
        }
      } finally {
        currentRegistry.set(null); // remove
      }
    }

    private void finalizerequest() throws IOException {
      if (reqMethod != null && serve.isAccessLogged()) {
        // TODO avoid hardcoded indecies, give them name as LOG_IP = 0,
        // LOG_IDENT = 1, LOG_REMOTE_USER = 2
        // 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, TODO get it from BASIC
                      // auth
        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++;
      closeStreams();
    }

    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 (reqSessionValue == null) // not from cookie
          reqSessionValue = sessionUrlValue;
        sessionValue = reqSessionValue;
        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.unloadServlet(servlete);
          servlete.destroy();
        } else if (e.getUnavailableSeconds() > 0)
          serve.log("TJWS: Temporary unavailability feature is not supported "
              + servlete);
        problem(e.getMessage(), SC_SERVICE_UNAVAILABLE);
      } catch (ServletException e) {
        serve.log("TJWS: 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("TJWS: 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("TJWS: " + logMessage);
      try {
        sendError(resCode, logMessage);
      } catch (IllegalStateException e) { /* ignore */
      } catch (IOException e) { /* ignore */
      }
    }

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

    public void spawnAsync(AsyncCallback setAsync) {
      // System.out.println("SPAWN==");
      asyncMode = setAsync;
    }

    public void joinAsync() {
      // System.out.println("JOIN==");
      synchronized (this) {
        if (asyncMode == null)
          return;
        asyncMode = null;
      }
      try {
        finalizerequest();
        if (keepAlive && serve.isKeepAlive()
            && timesRequested < serve.getMaxTimesConnectionUse())
          serve.threadPool.executeThread(this);
        else
          close();
      } catch (IOException ioe) {
        serve.log("TJWS: " + ioe);
      } finally {
      }
    }

    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;

    // TODO see if it can be simplified
    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("TJWS: 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("TJWS: Error in parsing cookies: " + cookies, e);
      } catch (Exception e) {
        serve.log("TJWS: 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();
          reqSessionValue = sessionCookieValue;
        } catch (IllegalStateException ise) {
        } catch (NullPointerException npe) {
        }
      } 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
                || (serve.proxySSL) ? "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 = 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() {
      String serverName = getHeader(HOST);
      if (serverName != null && serverName.length() > 0) {
        int colon = serverName.indexOf(':');
        if (colon >= 0)
          try {
            return Integer.parseInt(serverName.substring(colon + 1)
                .trim());
          } catch (NumberFormatException nfe) {

          }
        else {
          if ("https".equals(getScheme()))
            return 443;
          return 80;
        }
      }
      return socket.getLocalPort();
    }

    // / Returns the Internet Protocol (IP) address of the client or last
    // proxy that sent the request.
    // Same as the CGI variable REMOTE_ADDR.
    // Use header X-Forwarded-For to get actual address when proxy
    public String getRemoteAddr() {
      return socket.getInetAddress().getHostAddress();
    }

    // / Returns the fully qualified name of the client or the last proxy
    // that sent the request.
    //
    // Same as the CGI variable REMOTE_HOST.
    // Use header X-Forwarded-For to get actual address when proxy
    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 Map assureParametersFromRequest() {
      synchronized (resHeaderNames) { // supposes to not be null
        if (formParameters == null) {
          if ("GET".equals(reqMethod)) {
            if (reqQuery != null)
              try {
                formParameters = Acme.Utils.parseQueryString(
                    reqQuery, charEncoding);
              } catch (IllegalArgumentException ex) {
                serve.log("TJWS: 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 {
                  formParameters = Acme.Utils.parsePostData(
                      getContentLength(),
                      is = getInputStream(),
                      charEncoding, postCache);
                } catch (Exception ex) {
                  serve.log("TJWS: Exception "
                      + ex
                      + " at parsing 'POST' data of length "
                      + getContentLength());
                  // TODO propagate the exception ?
                  formParameters = EMPTYHASHTABLE;
                }
              } else
                formParameters = Acme.Utils.parseQueryString(
                    postCache[0], charEncoding);
              if (reqQuery != null && reqQuery.length() > 0)
                formParameters.putAll(Acme.Utils
                    .parseQueryString(reqQuery,
                        charEncoding));
            } else if (reqQuery != null)
              formParameters = Acme.Utils.parseQueryString(
                  reqQuery, charEncoding);
          }
        }
      }
      if (formParameters == null)
        formParameters = EMPTYHASHTABLE;
      return formParameters;
    }

    // / Returns the parameter names for this request.
    public Enumeration getParameterNames() {
      assureParametersFromRequest();
      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 String[] getParameterValues(String name) {
      assureParametersFromRequest();
      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) {
      name = name.toLowerCase();
      if (serve.proxyConfig && HOST.equals(name))
        name = "X-Forwarded-Host".toLowerCase();
      int i = reqHeaderNames.indexOf(name);
      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 reqSessionValue;
    }

    // / 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 (reqSessionValue != null) {
        AcmeSession session = serve.getSession(reqSessionValue);
        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 && sessionCookieValue == 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);
    }

    // / 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; // TODO check if stream was flushed
      }
    }

    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.
     *
     * 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 Map getParameterMap() {
      assureParametersFromRequest();
      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 getServerPort(); // 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.getCanonicalHostName(); /* 1.4 */
    }

    /**
     * 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.getHostAddress();
    }

    /**
     * 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 socket.getLocalPort();
    }

    // / 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;
        // new Exception("headers").printStackTrace();
        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);
        }
        if (wasContentLen == false && chunked_out == false
            && serve.isKeepAlive()) {
          out.println(TRANSFERENCODING + ": " + CHUNKED);
          chunked_out = true;
        }
        out.println();
        out.flush();
        ((ServeOutputStream) out).setChunked(chunked_out);
      }
    }

    // / 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();
    }

    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 rewriting
    // http://www.myserver.com/catalog/index.html;jsessionid=mysession1928
    // like:
    // http://www.sun.com/2001-0227/sunblade/;$sessionid$AD5RQ0IAADJAZAMTA1LU5YQ

    // / 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("TJWS: Problem in compressed stream creation",
                      ((InvocationTargetException) ex)
                          .getTargetException());
            else
              conn.serve
                  .log("TJWS: 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) {
        //if (STREAM_DEBUG) {
          //new Exception("Set content length:"+contentLength).printStackTrace();
          //}
        this.contentLength = contentLength;
        readCount = 0;
      } //else if (STREAM_DEBUG || true) {
        //new Exception("Igonore Set content length:"+contentLength+" for "+this.contentLength).printStackTrace();
      //}
    }

    /* ------------------------------------------------------------ */
    /**
     * 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(8192, 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 (readCount >= contentLength) {
            if (STREAM_DEBUG)
              System.err.print("EOF at "+contentLength);
            return -1;
          }
          if (contentLength - len < readCount  )
            len =  (int) (contentLength - readCount);
         
          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();
          else {
            out = null;
            conn = null; // the stream has to be recreated after
                    // closing
          }
        }
      } 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();
      if (root_node.object != null)
        result.addElement("/");
      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
   *
   * 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 Object getAttribute(String name) throws IllegalStateException {
      if (expired)
        throw new IllegalStateException();
      return get((Object) name);
    }

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

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

    public 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));
      }
      if (value != null) {
        if (value instanceof HttpSessionBindingListener)
          ((HttpSessionBindingListener) value)
              .valueBound(new HttpSessionBindingEvent(this, name));
        notifyListeners(name, oldValue, value);
      } else
        notifyListeners(name, oldValue);
    }

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

    public void removeAttribute(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));
        notifyListeners(name, value);
      }
    }

    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(i))
                  .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 notifyListeners(String name, Object value) {
      if (listeners != null) {
        HttpSessionBindingEvent event = new HttpSessionBindingEvent