// Serve - minimal Java HTTP server 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/

package Acme.Serve;

import java.io.*;
import java.util.*;
import java.net.*;
import java.text.*;
// import Acme.Serve.servlet.*;
// import Acme.Serve.servlet.http.*;
import javax.servlet.*;
import javax.servlet.http.*;

/// Minimal Java HTTP server class.
// <P>
// This class implements a very small embeddable HTTP server.
// It runs Servlets compatible with the API used by JavaSoft's
// <A HREF="http://java.sun.com/products/java-server/">JavaServer</A> server.
// 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 JavaServer.
// JavaServer is a full-fledged HTTP server and more.
// Acme.Serve is tiny, about 1500 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://java.sun.com/products/java-server/">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.gz">Fetch the entire Acme package.</A>
// <P>
// @see Acme.Serve.servlet.http.HttpServlet
// @see FileServlet
// @see CgiServlet

public class Serve implements ServletContext, Runnable
    {

    private static final String progName = "Serve";

    /// Main routine, if you want to run this directly as an application.
    public static void main( String[] args )
	{
	// Parse args.
	int port = 9090;
	String throttles = null;
	int argc = args.length;
	int argn;
	for ( argn = 0; argn < argc && args[argn].charAt( 0 ) == '-'; ++argn )
	    {
	    if ( args[argn].equals( "-p" ) && argn + 1 < argc )
		{
		++argn;
		port = Integer.parseInt( args[argn] );
		}
	    else if ( args[argn].equals( "-t" ) && argn + 1 < argc )
		{
		++argn;
		throttles = args[argn];
		}
	    else
		usage();
	    }
	if ( argn != argc )
	    usage();

	// Create the server.
	Serve serve = new Serve( port );

	// Any custom Servlets should be added here.
	serve.addServlet( "/SampleServlet", new Acme.Serve.SampleServlet() );
	Servlet ts = new Acme.Serve.TestServlet();
	serve.addServlet( "/TestServlet", ts );
	serve.addServlet( "/TestServlet/*", ts );

	// And add the standard Servlets.
	if ( throttles == null )
	    serve.addDefaultServlets( true );
	else
	    try
		{
		serve.addDefaultServlets( true, throttles );
		}
	    catch ( IOException e )
		{
		System.err.println( "Problem reading throttles file: " + e );
		System.exit( 1 );
		}

	// And run.
	serve.serve();

	System.exit( 0 );
	}

    private static void usage()
	{
	System.err.println( "usage:  " + progName + " [-p port]" );
	System.exit( 1 );
	}


    private int port;
    private PrintStream logStream;
    Acme.WildcardDictionary registry;
    Properties props;

    /// Constructor.
    public Serve( int port, PrintStream logStream, Properties props )
	{
	this.port = port;
	this.logStream = logStream;
	this.props = props;
	registry = new Acme.WildcardDictionary();
	}

    /// Constructor.
    public Serve( int port, PrintStream logStream )
	{
	this( port, logStream, new Properties());
	}

    /// Constructor, default log stream.
    public Serve( int port, Properties props )
	{
	this( port, System.err, props );
	}


    /// Constructor, default log stream.
    public Serve( int port )
	{
	this( port, System.err, new Properties() );
	}

    /// Constructor, default port and log stream.
    // We don't use 80 as the default port because we don't want to
    // encourage people to run a Java web server as root because Java
    // currently has no way of giving up root privs!  Instead, the
    // current default port is 9090.
    public Serve()
	{
	this( 9090, System.err, new Properties());
	}


    /// 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 )
	{
	// See if we have already instantiated this one.
	Servlet servlet = (Servlet) servlets.get( className );
	if ( servlet != null )
	    {
	    addServlet( urlPat, servlet );
	    return;
	    }

	// Check if we're allowed to make one of these.
	SecurityManager security = System.getSecurityManager();
	if ( security != null )
	    {
	    int i = className.lastIndexOf( '.' );
	    if ( i != -1 )
		{
		security.checkPackageAccess(
		    className.substring( 0, i ) );
		security.checkPackageDefinition(
		    className.substring( 0, i ) );
		}
	    }

	// Make a new one.
	try
	    {
	    servlet = (Servlet) Class.forName( className ).newInstance();
	    addServlet( urlPat, servlet );
	    return;
	    }
	catch ( ClassNotFoundException e )
	    {
	    log( "Class not found: " + className );
	    }
	catch ( ClassCastException e )
	    {
	    log( "Class cast problem: " + e.getMessage() );
	    }
	catch ( InstantiationException e )
	    {
	    log( "Instantiation problem: " + e.getMessage() );
	    }
	catch ( IllegalAccessException e )
	    {
	    log( "Illegal class access: " + e.getMessage() );
	    }
	catch ( Exception e )
	    {
	    log( "Unexpected problem creating servlet: " + 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.
    public void addServlet( String urlPat, Servlet servlet )
	{
	try
	    {
	    servlet.init( new ServeConfig( (ServletContext) this ) );
	    registry.put( urlPat, servlet );
	    servlets.put( servlet.getClass().getName(), servlet );
	    }
	catch ( ServletException e )
	    {
	    log( "Problem initializing servlet: " + e );
	    }
	}

    public void removeServlet( String urlPat )
	{
	registry.remove (urlPat);
	}

    /// 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
    public void addDefaultServlets( boolean cgi )
	{
	if ( cgi )
	    addServlet( "*.cgi", new Acme.Serve.CgiServlet() );
	addServlet( "*", new Acme.Serve.FileServlet() );
	}

    /// Register a standard set of Servlets, with throttles.
    // @param cgi whether to run CGI programs
    // @param throttles filename to read FileServlet throttle settings from
    public void addDefaultServlets( boolean cgi, String throttles ) throws IOException
	{
	if ( cgi )
	    addServlet( "*.cgi", new Acme.Serve.CgiServlet() );
	addServlet( "*", new Acme.Serve.FileServlet( throttles ) );
	}

    public void run()
	{
	serve();
	}


    /// Run the server.  Returns only on errors.
    public void serve()
	{
	ServerSocket serverSocket;
	try
	    {
	    serverSocket = new ServerSocket( port, 1000 );
	    }
	catch ( IOException e )
	    {
	    log( "Server socket: " + e );
	    return;
	    }

	try
	    {
	    while ( true )
		{
		Socket socket = serverSocket.accept();
		new ServeConnection( socket, this );
		}
	    }
	catch ( IOException e )
	    {
	    log( "Accept: " + e );
	    }
	finally
	    {
	    try
		{
		serverSocket.close();
		destroyAllServlets();
		}
	    catch ( IOException e ) {}
	    }
	}


    // Methods from ServletContext.

    protected Hashtable servlets = new Hashtable();

    /// Gets a servlet by name.
    // @param name the servlet name
    // @return null if the servlet does not exist
    public Servlet getServlet( String name )
	{
	return (Servlet) servlets.get( name );
	}

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

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

    /// Destroys all currently-loaded servlets.
    public void destroyAllServlets()
	{
	Enumeration en = servlets.elements();
	while ( en.hasMoreElements() )
	    {
	    Servlet servlet = (Servlet) en.nextElement();
	    servlet.destroy();
	    }
	servlets.clear();
	}

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

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

    /// 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 )
	{
	// No mapping.
	return path;
	}

    /// Returns the MIME type of the specified file.
    // @param file file name whose MIME type is required
    public String getMimeType( String file )
	{
	int lastDot = file.lastIndexOf( '.' );
	int lastSep = file.lastIndexOf( File.separatorChar );
	if ( lastDot == -1 ||
	     ( lastSep != -1 && lastDot < lastSep ) )
	    return "text/plain";
	String extension = file.substring( lastDot + 1 );
	if ( extension.equals( "html" ) || extension.equals( "htm" ) )
	    return "text/html";
	if ( extension.equals( "gif" ) )
	    return "image/gif";
	if ( extension.equals( "jpg" ) || extension.equals( "jpeg" ) )
	    return "image/jpeg";
	if ( extension.equals( "au" ) )
	    return "audio/basic";
	if ( extension.equals( "ra" ) || extension.equals( "ram" ) )
	    return "audio/x-pn-realaudio";
	if ( extension.equals( "wav" ) )
	    return "audio/wav";
	if ( extension.equals( "mpg" ) || extension.equals( "mpeg" ) )
	    return "video/mpeg";
	if ( extension.equals( "qt" ) || extension.equals( "mov" ) )
	    return "video/quicktime";
	if ( extension.equals( "class" ) )
	    return "application/octet-stream";
	if ( extension.equals( "ps" ) )
	    return "application/postscript";
	if ( extension.equals( "wrl" ) )
	    return "x-world/x-vrml";
	if ( extension.equals( "pac" ) )
	    return "application/x-ns-proxy-autoconfig";
	return "text/plain";
	}

    /// 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 ServeUtils.serverName + " " + ServeUtils.serverVersion +
	       " (" + ServeUtils.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 )
	{
	// This server does not support attributes.
	return null;
	}

    }


class ServeConfig implements ServletConfig
    {

    private ServletContext context;

    public ServeConfig( ServletContext context )
	{
	this.context = context;
	}

    // 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 doesn't support servlet init params.
	return null;
	}

    /// Gets the names of the initialization parameters of the servlet.
    // @param name the parameter name
    public Enumeration getInitParameterNames()
	{
	// This server doesn't support servlet init params.
	return new Vector().elements();
	}

    }


class ServeConnection implements Runnable, HttpServletRequest, HttpServletResponse
    {

    private Socket socket;
    private Serve serve;

    private ServletInputStream in;
    private ServletOutputStream out;

    private Vector cookies = new Vector(); // !!!


    /// Constructor.
    public ServeConnection( Socket socket, Serve serve )
	{
	// Save arguments.
	this.socket = socket;
	this.serve = serve;

	// Start a separate thread to read and handle the request.
	Thread thread = new Thread( this );
	thread.start();
	}


    // Methods from Runnable.

    private String reqMethod = null;
    private String reqUriPath = null;
    private String reqProtocol = null;
    private boolean oneOne;		// HTTP/1.1 or better
    private boolean reqMime;
    String reqQuery = null;
    private Vector reqHeaderNames = new Vector();
    private Vector reqHeaderValues = new Vector();

    public void run()
	{
	try
	    {
	    // Get the streams.
	    in = new ServeInputStream( socket.getInputStream() );
	    out = new ServeOutputStream( socket.getOutputStream(), this );
	    }
	catch ( IOException e )
	    {
	    problem( "Getting streams: " + e.getMessage(), SC_BAD_REQUEST );
	    }

	parseRequest();

	// FIXME:
	// There's a strange bug with Netscape/Unix where NS laments
	// that the peer closed the connection when POST requests are
	// redirected. Waiting for one second seems to fix the problem.
	/* try
	    {
	    Thread.currentThread().sleep (1000l);
	    }
	catch (InterruptedException ignore) {} */
	
	try
	    {
	    socket.close();
	    }
	catch ( IOException e ) { /* ignore */ }
	}

    private void parseRequest()
	{
	byte[] lineBytes = new byte[4096];
	int len;
	String line;

	try
	    {
	    // Read the first line of the request.
	    len = in.readLine( lineBytes, 0, lineBytes.length );
	    if ( len == -1 || len == 0 )
		{
		problem( "Empty request", SC_BAD_REQUEST );
		return;
		}
	    line = new String( lineBytes, 0, len );
	    String[] tokens = Acme.Utils.splitStr( line );
	    switch ( tokens.length )
		{
		case 2:
		// Two tokens means the protocol is HTTP/0.9.
		reqProtocol = "HTTP/0.9";
		oneOne = false;
		reqMime = false;
		break;
		case 3:
		reqProtocol = tokens[2];
		oneOne = ! reqProtocol.toUpperCase().equals( "HTTP/1.0" );
		reqMime = true;
		// Read the rest of the lines.
		while ( true )
		    {
		    len = in.readLine( lineBytes, 0, lineBytes.length );
		    if ( len == -1 || len == 0 )
			break;
		    line = new String( lineBytes, 0, len );
		    int colonBlank = line.indexOf( ": " );
		    if ( colonBlank != -1 )
			{
			String name = line.substring( 0, colonBlank );
			String value = line.substring( colonBlank + 2 );
			reqHeaderNames.addElement( name.toLowerCase() );
			reqHeaderValues.addElement( value );
			}
		    }
		break;
		default:
		problem( "Malformed request line", SC_BAD_REQUEST );
		return;
		}
	    reqMethod = tokens[0];
	    reqUriPath = tokens[1];

	    // Check Host: header in HTTP/1.1 requests.
	    if ( oneOne )
		{
		String host = getHeader( "host" );
		if ( host == null )
		    {
		    problem(
			"Host header missing on HTTP/1.1 request",
			SC_BAD_REQUEST );
		    return;
		    }
		// !!!
		}

	    // Split off query string, if any.
	    int qmark = reqUriPath.indexOf( '?' );
	    if ( qmark != -1 )
		{
		reqQuery = reqUriPath.substring( qmark + 1 );
		reqUriPath = reqUriPath.substring( 0, qmark );
		}

	    // Decode %-sequences.
	    reqUriPath = decode( reqUriPath );
	    if (reqQuery != null)
	    	reqQuery = decode (reqQuery);
	    Servlet servlet = (Servlet) serve.registry.get( reqUriPath );
	    if ( servlet != null )
		runServlet( (HttpServlet) servlet );
	    else if ( "/".equals( reqUriPath ))
		sendRedirect (serve.props.getProperty ("rootapp", "base"));
	    else if ( !reqUriPath.endsWith ("/"))
	 	sendRedirect (reqUriPath+"/");
	    else // Not found
	             sendError (404, "Not Found",
		"<p>If you are looking for a specific app, try <tt>/appname</tt>.</p>"+
		"<p>If the URL was generated by the Hop's href() method "+
	              "check if the <tt>baseURI</tt> property is set correctly in the app's <tt>app.properties</tt> file.</p>");
	    }
	catch ( IOException e )
	    {
	    problem( "Reading request: " + e.getMessage(), SC_BAD_REQUEST );
	    }
	}

    private void runServlet( HttpServlet servlet )
	{
	// Set default response fields.
	setStatus( SC_OK );
	setDateHeader( "Date", System.currentTimeMillis() );
	setHeader(
	    "Server", ServeUtils.serverName + "/" + ServeUtils.serverVersion );
	setHeader( "Connection", "close" );
	try
	    {
	    servlet.service( this, this );
	    }
	catch ( IOException e )
	    {
	    problem(
		"IO problem running servlet: " + e.toString(), SC_BAD_REQUEST );
	    }
	catch ( ServletException e )
	    {
	    problem(
		"problem running servlet: " + e.toString(), SC_BAD_REQUEST );
	    }
	catch ( Exception e )
	    {
	    problem(
		"unexpected problem running servlet: " + e.toString(),
		SC_INTERNAL_SERVER_ERROR );
	    }
	}

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

    private String decode( String str )
	{
	StringBuffer result = new StringBuffer();
	int l = str.length();
	for ( int i = 0; i < l; ++i )
	    {
	    char c = str.charAt( i );
	    if ( c == '%' && i + 2 < l )
		{
		char c1 = str.charAt( i + 1 );
		char c2 = str.charAt( i + 2 );
		if ( isHexit( c1 ) && isHexit( c2 ) )
		    {
		    result.append( (char) ( hexit( c1 ) * 16 + hexit( c2 ) ) );
		    i += 2;
		    }
		else
		    result.append( c );
		}
	    else if ( c == '+' )
	    	result.append( ' ' );
	    else
		result.append( c );
	    }
	return result.toString();
	}

    private boolean isHexit( char c )
	{
	String legalChars = "0123456789abcdefABCDEF";
	return ( legalChars.indexOf( c ) != -1 );
	}

    private int hexit( char c )
	{
	if ( c >= '0' && c <= '9' )
	    return c - '0';
	if ( c >= 'a' && c <= 'f' )
	    return c - 'a' + 10;
	if ( c >= 'A' && c <= 'F' )
	    return c - 'A' + 10;
	return 0;	// shouldn't happen, we're guarded by isHexit()
	}


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

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

    /// 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()
	{
	return "http";
	}

    /// 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()
	{
	try
	    {
	    return InetAddress.getLocalHost().getHostName();
	    }
	catch ( UnknownHostException e )
	    {
	    return null;
	    }
	}

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

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

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

    /// 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
	{
	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()
	{
	// !!!
	return null;
	}

    Hashtable parameters;

    protected void parseParams() {

        // Have we already done it?
        if (parameters != null) {
            return;
        }

        // Parse any query string parameters from the request
        Hashtable queryParameters = null;
        try {
            queryParameters = HttpUtils.parseQueryString(getQueryString());
        } catch (IllegalArgumentException e) {
            queryParameters = null;
        }

        // Parse any posted parameters in the input stream
        Hashtable postParameters = null;
        if ("POST".equals(getMethod()) &&
            "application/x-www-form-urlencoded".equals(getContentType())) {
            try {
                ServletInputStream is = getInputStream();
                postParameters =
                    HttpUtils.parsePostData(getContentLength(), in);
            } catch (IllegalArgumentException e) {
                postParameters = null;
            } catch (IOException e) {
                postParameters = null;
            }
        }

        // Handle the simple cases that require no merging
        if ((queryParameters == null) && (postParameters == null)) {
            parameters = new Hashtable();
            return;
        } else if (queryParameters == null) {
            parameters = postParameters;
            return;
        } else if (postParameters == null) {
            parameters = queryParameters;
            return;
        }

        // Merge the parameters retrieved from both sources
        Enumeration postKeys = postParameters.keys();
        while (postKeys.hasMoreElements()) {
            String postKey = (String) postKeys.nextElement();
            Object postValue = postParameters.get(postKey);
            Object queryValue = queryParameters.get(postKey);
            if (queryValue == null) {
                queryParameters.put(postKey, postValue);
                continue;
            }
            Vector queryValues = new Vector();
            if (queryValue instanceof String) {
                queryValues.addElement(queryValue);
            } else if (queryValue instanceof String[]) {
                String queryArray[] = (String[]) queryValue;
                for (int i = 0; i < queryArray.length; i++) {
                    queryValues.addElement(queryArray[i]);
                }
            }
            if (postValue instanceof String) {
                queryValues.addElement(postValue);
            } else if (postValue instanceof String[]) {
                String postArray[] = (String[]) postValue;
                for (int i = 0; i < postArray.length; i++) {
                    queryValues.addElement(postArray[i]);
                }
            }
            String queryArray[] = new String[queryValues.size()];
            for (int i = 0; i < queryArray.length; i++) {
                queryArray[i] = (String) queryValues.elementAt(i);
            }
            queryParameters.put(postKey, queryArray);
        }
        parameters = queryParameters;

    }


    /// Returns the parameter names for this request.
    public Enumeration getParameterNames() {
	parseParams();
	return parameters.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) {

        parseParams();

        Object val = parameters.get(name);

        if (val == null) {
            return null;
        } else if (val instanceof String[]) {
            // It's an array, return the first element
            return ((String[])val)[0];
        } else {
            // It's a string so return it
            return (String) val;
        }
    }

    /// 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) {

        parseParams();

        Object val = parameters.get(name);

        if (val == null) {
            return null;
        } else if (val instanceof String) {
            // It's a string, convert to an array and return
            String va[] = {(String) val};
            return va;
        } else {
            // It's an array so return it
            return (String[]) val;
        }
    }

    /// 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 )
	{
	// This server does not implement attributes.
	return null;
	}


    // Methods from HttpServletRequest.

    /// Gets the array of cookies found in this request.
    public Cookie[] getCookies()
	{
	return parseCookieHeader(getHeader("Cookie"));
	}

    /**
     * Parse a cookie header into an array of cookies as per
     * RFC2109 - HTTP Cookies
     *
     * @param cookieHdr The Cookie header value.
     */
    public Cookie[] parseCookieHeader(String cookieHdr) {
        Vector cookieJar = new Vector();

        if(cookieHdr == null || cookieHdr.length() == 0)
            return new Cookie[0];

        StringTokenizer stok = new StringTokenizer(cookieHdr, "; ");
        while (stok.hasMoreTokens()) {
            try {
                String tok = stok.nextToken();
                int equals_pos = tok.indexOf('=');
                if (equals_pos > 0) {
                    String name = decode(tok.substring(0, equals_pos));
                    String value = decode(tok.substring(equals_pos + 1));
                    cookieJar.addElement(new Cookie(name, value));
                }
                else if ( tok.length() > 0 && equals_pos == -1 ) {
                    String name = decode(tok);
                    cookieJar.addElement(new Cookie(name, ""));
                }
            } catch (IllegalArgumentException badcookie) {
            } catch (NoSuchElementException badcookie) {
            }
        }

        Cookie[] cookies = new Cookie[cookieJar.size()];
        cookieJar.copyInto(cookies);
        return cookies;
    }


    /// 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 full request URI.
    public String getRequestURI()
	{
	String portPart = "";
	int port = getServerPort();
	if ( port != 80 )
	    portPart = ":" + port;
	String queryPart = "";
	String queryString = getQueryString();
	if ( queryString != null && queryString.length() > 0 )
	    queryPart = "?" + queryString;
	return "http://" + getServerName() + portPart + reqUriPath + queryPart;
	}

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

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

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

    /// 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()
	{
	// This server does not support authentication, so even if a username
	// is supplied in the headers we don't want to look at it.
	return null;
	}

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

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

    /// Returns the value of an integer header field.
    // @param name the header field name
    // @param def the integer value to return if header not found or invalid
    public int getIntHeader( String name )
	{
	String val = getHeader( name );
	try
	    {
	    return Integer.parseInt( val );
	    }
	catch ( Exception e )
	    {
	    return -1;
	    }
	}

    /// Returns the value of a long header field.
    // @param name the header field name
    public long getLongHeader( String name )
	{
	String val = getHeader( name );
	try
	    {
	    return Long.parseLong( val );
	    }
	catch ( Exception e )
	    {
	    return -1l;
	    }
	}

    /// Returns the value of a date header field.
    // @param name the header field name
    public long getDateHeader( String name )
	{
	String val = getHeader( name );
	try
	    {
	    return DateFormat.getDateInstance().parse( val ).getTime();
	    }
	catch ( Exception e )
	    {
	    return -1l;
	    }
	}

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

    // Session stuff.  Not implemented, but the API is here for compatibility.

    /// 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 HttpSession getSession( boolean create )
	{
	return null;
	}

    /// 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()
	{
	String sid = ServeUtils.getCookie (this, "HopSession");
	if (sid == null) {
		sid = Long.toString (Math.round (Math.random ()*Long.MAX_VALUE), 16);
		ServeUtils.setCookie (this, "HopSession",  sid);
	}
	return sid;
	}

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

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


    // Methods from ServletResponse.

    /// Sets the content length for this response.
    // @param length the content length
    public void setContentLength( int length )
	{
	setIntHeader( "Content-length", length );
	}

    /// Sets the content type for this response.
    // @param type the content type
    public void setContentType( String type )
	{
	setHeader( "Content-type", type );
	}

    /// Returns an output stream for writing response data.
    public ServletOutputStream getOutputStream()
	{
	return out;
	}

    /// 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
	{
	// !!!
	return new PrintWriter (new OutputStreamWriter (out, "ISO8859_1"));
	}

    /// 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()
	{
	// !!!
	return null;
	}


    // 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 )
	{
	cookies.addElement( cookie );
	}
    
    /// Checks whether the response message header has a field with the
    // specified name.
    public boolean containsHeader( String name )
	{
	return resHeaderNames.contains( name );
	}

    private int resCode = -1;
    private String resMessage = null;
    private Vector resHeaderNames = new Vector();
    private Vector resHeaderValues = new Vector();

    /// 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 )
	{
	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 large" ); 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;
	    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 name, String value )
	{
	resHeaderNames.addElement( name );
	resHeaderValues.addElement( value );
	}

    /// 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 name, int value )
	{
	setHeader( name, 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 name, long value )
	{
	setHeader( name, 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 name, long value )
	{
	setHeader( name, to1123String( new Date( value ) ) );
	}

    private static final String[] weekdays =
	{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

    /// Converts a Date into an RFC-1123 string.
    private static String to1123String( Date date )
	{
	// We have to go through some machinations here to get the
	// correct day of the week in GMT.  getDay() gives the day in
	// local time.  getDate() gives the day of the month in local
	// time.  toGMTString() gives a formatted string in GMT.  So, we
	// extract the day of the month from the GMT string, and if it
	// doesn't match the local one we change the local day of the
	// week accordingly.
	//
	// The Date class sucks.
	int localDay = date.getDay();
	int localDate = date.getDate();
	String gmtStr = date.toGMTString();
	int blank = gmtStr.indexOf( ' ' );
	int gmtDate = Integer.parseInt( gmtStr.substring( 0, blank ) );
	int gmtDay;
	if ( gmtDate > localDate || ( gmtDate < localDate && gmtDate == 1 ) )
	    gmtDay = ( localDay + 1 ) % 7;
	else if ( localDate > gmtDate || ( localDate < gmtDate && localDate == 1 ) )
	    gmtDay = ( localDay + 6 ) % 7;
	else
	    gmtDay = localDay;
	return weekdays[gmtDay] + ( gmtDate < 10 ? ", 0" : ", " ) + gmtStr;
	}

    private boolean headersWritten = false;

    /// 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
	{
	if ( headersWritten )
	    return;
	headersWritten = true;
	if ( reqMime )
	    {
	    out.println( reqProtocol + " " + resCode + " " + resMessage );
	    for ( int i = 0; i < resHeaderNames.size(); ++i )
		{
		String name = (String) resHeaderNames.elementAt( i );
		String value = (String) resHeaderValues.elementAt( i );
		if ( value != null )	// just in case
		    out.println( name + ": " + value );
		}
	    writeCookies();
	    out.println( "" );
	    out.flush();
	    }
	}

    void writeCookies() throws IOException
	{
	// Send the cookies
	Enumeration enum = cookies.elements();
	while (enum.hasMoreElements())
	    {
	    Cookie cookie = (Cookie) enum.nextElement();
	    String cookieHdr = "Set-Cookie: " + encodeCookie(cookie);
	    out.println(cookieHdr);
	    }
	}

    /// Encode Cookie. Borrowed from Apache JServ.
    private static SimpleDateFormat cookieDate =
        new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss zz", Locale.US );
    public static String encodeCookie(Cookie cookie) {
	StringBuffer buf = new StringBuffer( cookie.getName() );
	buf.append('=');
	buf.append(cookie.getValue());

	long age = cookie.getMaxAge();
	if (age > 0) {
	    buf.append("; expires=");
	    buf.append(cookieDate.format(
	                new Date(System.currentTimeMillis() + (long)age * 1000 )));
	} else if (age == 0) {
	    buf.append("; expires=");
	    // Set expiration to the epoch to delete the cookie
	    buf.append(cookieDate.format(new Date(0)));
	}

	if (cookie.getDomain() != null) {
	    buf.append("; domain=");
	    buf.append(cookie.getDomain());
	}

	if (cookie.getPath() != null) {
	    buf.append("; path=");
	    buf.append(cookie.getPath());
	}

	if (cookie.getSecure()) {
	    buf.append("; secure");
	}

	return buf.toString();
    }

    /// 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, String resBody ) throws IOException
	{
	setStatus( resCode, resMessage );
	realSendError(resBody);
	}

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

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

    private void realSendError( String resBody ) throws IOException
	{
	setContentType( "text/html" );
	out.println( "<HTML><HEAD>" );
	out.println( "<TITLE>" + resCode + " " + resMessage + "</TITLE>" );
	out.println( "</HEAD><BODY BGCOLOR=\"#FFFFFF\">" );
	out.println( "<H2>" + resCode + " " + resMessage + "</H2>" );
	if (resBody != null)
	    out.println (resBody);
	out.println( "<HR>" );
	ServeUtils.writeAddress( out );
	out.println( "</BODY></HTML>" );
	out.flush();
	}

    /// 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
	{
	setHeader( "Location", location );
	sendError (SC_MOVED_TEMPORARILY);
	}

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

    /// Encodes the specified URL by including the session ID in it, or, if
    // encoding is not needed, returns the URL unchanged. The
    // implementation of this method should include the logic to determine
    // whether the session ID needs to be encoded in the URL. For example,
    // if the browser supports cookies, or session tracking is turned off,
    // URL encoding is unnecessary.
    // <P>
    // All URLs emitted by a Servlet should be run through this method.
    // Otherwise, URL rewriting cannot be used with browsers which do not
    // support cookies.
    public String encodeUrl( String url )
	{
	return 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 url;
	}

    }


class ServeInputStream extends ServletInputStream
    {

    private InputStream in;

    public ServeInputStream( InputStream in )
	{
	this.in = in;
	}

    public int readLine( byte[] b, int off, int len ) throws IOException
	{
	int off2 = off;
	while ( off2 - off < len )
	    {
	    int r = read();
	    if ( r == -1 )
		{
		if (off2 == off )
		    return -1;
		break;
		}
	    if ( r == 13 )
		continue;
	    if ( r == 10 )
		break;
	    b[off2] = (byte) r;
	    ++off2;
	    }
	return off2 - off;
	}

    public int read() throws IOException
	{
	return in.read();
	}

    public int read( byte[] b, int off, int len ) throws IOException
	{
	return in.read( b, off, len );
	}

    public int available() throws IOException
	{
	return in.available();
	}

    public void close() throws IOException
	{
	in.close();
	}

    }


class ServeOutputStream extends ServletOutputStream
    {

    private OutputStream out;
    private ServeConnection conn;

    public ServeOutputStream( OutputStream out, ServeConnection conn )
	{
	this.out = out;
	this.conn = conn;
	}

    public void write( int b ) throws IOException
	{
	conn.writeHeaders();
	out.write( b );
	}

    public void write( byte[] b, int off, int len ) throws IOException
	{
	conn.writeHeaders();
	out.write( b, off, len );
	}

    public void flush() throws IOException
	{
	conn.writeHeaders();
	out.flush();
	}

    public void close() throws IOException
	{
	conn.writeHeaders();
	out.close();
	}

    public void print( String s ) throws IOException
	{
	conn.writeHeaders();
	out.write( s.getBytes() );
	}

    public void print( int i ) throws IOException
	{
	conn.writeHeaders();
	out.write( Integer.toString(i).getBytes() );
	}

    public void print( long l ) throws IOException
	{
	conn.writeHeaders();
	out.write( Long.toString(l).getBytes() );
	}

    public void println( String s ) throws IOException
	{
	conn.writeHeaders();
	out.write( s.getBytes() );
	out.write ("\r\n".getBytes());
	}

    public void println( int i ) throws IOException
	{
	conn.writeHeaders();
	out.write( Integer.toString(i).getBytes() );
	out.write ("\r\n".getBytes());
	}

    public void println( long l ) throws IOException
	{
	conn.writeHeaders();
	out.write( Long.toString(l).getBytes() );
	out.write ("\r\n".getBytes());
	}

    public void println() throws IOException
	{
	conn.writeHeaders();
	out.write ("\r\n".getBytes());
	}

    }
