// HttpBasicConnection.java
// $Id: HttpBasicConnection.java,v 1.24 2004/02/11 15:25:18 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.www.protocol.http;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import java.net.InetAddress;
import java.net.Socket;

import org.w3c.www.mime.MimeParser;
import org.w3c.www.mime.MimeParserFactory;

import org.w3c.www.http.HttpStreamObserver;

class HttpBasicConnection extends HttpConnection implements HttpStreamObserver
{
    private static final boolean debug = false;

    static Method sock_m = null;
    static {
	try {
	    Class c = java.net.Socket.class;
	    sock_m = c.getMethod("isClosed", null);
	} catch (NoSuchMethodException ex) {
	    // not using a recent jdk...
	    sock_m = null;
	}
    }

    // A small class that start a thread to create a socket
    // while the original thread waits only for some amount of time
    // and indicate the other thread (if not finished) that the newly
    // created socket can be destroyed if it took too long.
    private class TimedSocket implements Runnable {
	Socket _sock = null;
	InetAddress _inetaddr = null;
	int _port = 0;
	boolean _ok = true;
	IOException _ioex = null;

	public synchronized Socket getSocket(InetAddress inetaddr, int port) 
	    throws IOException
	{
	    _inetaddr = inetaddr;
	    _port = port;
	    Thread t = new Thread(this);
	    t.start();

	    try {
                t.join(connect_timeout);
            } catch (InterruptedException iex){
		_ok = false;
		if (_sock != null) {
		    try {
			_sock.close();
		    } catch (IOException ioex) {};
		    _sock = null;
		}
            }
	    if (_sock != null) {
		return _sock;
	    }
	    _ok = false;
	    // should we interrupt the other thread here?
	    if (_ioex != null) {
		throw _ioex;
	    } else {
		throw new IOException("Connect timed out");
	    }
	}

	public void run() {
	    Socket s = null;
	    try {
		s = new Socket(inetaddr, port);
	    } catch (IOException ex) {
		_ioex = ex;
	    }
	    if (_ok) {
		_sock = s;
	    } else {
		try {
		    if (s != null) {
			s.close();
		    }
		} catch (IOException ioex) {};
	    }
	}
    }

    /**
     * The physical socket underlying the connection.
     */
    private Socket       socket = null;
    /**
     * The MIME parser to read input from the connection.
     */
    MimeParser   parser = null;
    /**
     * THe socket output stream, when available.
     */
    OutputStream output = null;
    /**
     * The socket input stream when available.
     */
    InputStream  input  = null;
    /**
     * The MimeParser factory to use to create Reply instances.
     */
    MimeParserFactory reply_factory = null;
    /**
     * The thread that owns the connection, for checking assertions.
     */
    Thread th = null;
    /**
     * The target INET address of this connection.
     */
    InetAddress inetaddr = null;
    /**
     * The target port number for this connection.
     */
    int         port     = -1;
    /**
     * The Timout on the underlying socket
     */
    int        timeout   = 300000;
    /**
     * The Connection timeout for the underlying socket
     */
    int       connect_timeout = 1000;
    /**
     * All connections are associated with a uniq identifier, for debugging.
     */
    protected int id = -1;
    /**
     * if a close is needed at the end of the connection (ie: on a
     * Connection: close client or server side
     */
    protected boolean closeOnEOF = false;

    protected synchronized void setCloseOnEOF(boolean doit) {
	closeOnEOF = doit;
    }

    /**
     * Print this connection into a String.
     * @return A String containing the external representation for the 
     * connection.
     */

    public String toString() {
	return inetaddr + ":" + port +"["+id+"]";
    }

    /**
     * The entity stream we observe has reached its end.
     * Notify the server that it can now reuse the connection safely for some
     * other pending requests.
     * @param in The stream that has reached its end of file.
     */

    public synchronized void notifyEOF(InputStream in) {
	markIdle(closeOnEOF);
    }

    /**
     * The entity stream we were to observe refuse to be observed.
     * The connection will not be reusable, so we should detach it from the 
     * managing server, without closing it, since the entity reader will
     * close it itself.
     * @param in The stream that has been closed.
     */

    public synchronized void notifyFailure(InputStream in) {
	markIdle(true);
    }

    /**
     * The entity stream we observe has been closed.
     * After making sure the entire entity has been read, we can safely hand
     * out the connection to the server, for later reuse.
     * @param in The stream that has been closed.
     */

    public synchronized void notifyClose(InputStream in) {
	boolean _close = false;
	try {
	    if (in.available() > 0) {
		_close = true;
	    }
//	try {
//	    byte buffer[] = new byte[1024];
//	    while (in.read(buffer) >= 0) {
//	    }
	} catch (IOException ex) {
	    _close = true;
	} finally {
	    markIdle(_close);
	}
	
    }

    /**
     * Close this connection to terminate it.
     * This method will only close the streams, and free all the data
     * structures that it keeps.
     */

    public synchronized void close() {
	close(false);
    }

    
    /**
     * Close this connection to terminate it.
     * This method will only close the streams, and free all the data
     * structures that it keeps.
     */

    private synchronized void close(boolean force) {
	if ( socket != null || force) {
	    // Close the socket:
	    try {
		if (socket != null) {
		    socket.close();
		}
	    } catch (IOException ex) {
	    }
	    socket = null;
	    // Mark all data as invalid:
	    output = null;
	    input  = null;
	    parser = null;
	    cached = false;
	    th = null;
	    // Mark that connection as dead:
	    ((HttpBasicServer) server).deleteConnection(this);
	}
    }

    /**
     * Mark this connection as being used.
     * The server, which keeps track of idle connections, has decided to use
     * this connection to run some request. Mark this connection as used
     * and unregister it from the server's list of idle connections.
     * <p>Some assumptions are checked before handing out the connection
     * for use, which can throw an RuntimeException.
     * @return A boolean, <strong>true</strong> if the connection can be used
     * or reused, <strong>false</strong> otherwise (the connection was detected
     * idle, and destroy itself).
     * @exception RuntimeException If the connection is in an invalid state.
     */
    
    public boolean markUsed() {
	cached = false;
	if ( debug )
	    System.out.println(this+ " used !");
	if ( th != null )
	    throw new RuntimeException(this+" already used by "+th);
	th = Thread.currentThread();
	if ( socket != null ) {
	    cached = true;
	    if (sock_m != null) {
		try {
		    // check if socket is good using jdk1.4 feature
		    // would be faster to do it natively...
		    Boolean b = (Boolean) sock_m.invoke(socket, null);
		    if (debug) {
			System.out.println("Socket is closed? "+b);
		    }
		    if (b.booleanValue()) {
			try {
			    socket.close();
			} catch (IOException ieox) {}
			socket = null;
		    }
		} catch (InvocationTargetException ex) {
		    if (debug)
			ex.printStackTrace();
		    // weird, let's close it
		    socket = null;
		} catch (IllegalAccessException ex) {
		    // weird also here :)
		    if (debug)
			ex.printStackTrace();	    
		    socket = null;
		} catch (Exception fex) {
		    socket = null;
		}
	    }
	}
	// damn... jdk1.4 is not behaving as it should :(
	if ( socket == null ) {
	    try {
		TimedSocket ts = new TimedSocket();
		socket = ts.getSocket(inetaddr, port);
		socket.setSoTimeout(timeout);
		output = new BufferedOutputStream(socket.getOutputStream());
		input  = new BufferedInputStream(socket.getInputStream());
		parser = new MimeParser(input, reply_factory);
		cached = false;
	    } catch (Throwable ex) {
		if (debug) {
		    ex.printStackTrace();
		}
		// Close that connection (cleanup):
		close(true); 
		return false;
	    }
	} 
	return true;
    }

    /**
     * The connection is now idle again.
     * Mark the connection as idle, and register it to the server's list of 
     * idle connection (if this connection can be reused). If the connection
     * cannot be reused, detach it from the server and forget about it (the
     * caller will close it by closing the entity stream).
     * @param close Should this connection be physically closed (it is not
     * reusable), or should we try to keep track of it for later reuse.
     * @exception RuntimeException If the connection is in an invalid state.
     */

    public void markIdle(boolean close) {
	// Has this connection already been marked idle ?
	synchronized (this) {
	    if ( th == null ) 
		return;
	    if ( debug )
		System.out.println(this+" idle !"+close);
	// Check consistency:
	// if ( Thread.currentThread() != th )
	//    throw new RuntimeException(this +
	//	 		         " th mismatch " +
	//		 	         th + 
	//			         "/" +
	//			         Thread.currentThread());
	// Ok, mark idle for good:
	    th = null;
	}
	if ( close ) {
	    if ( debug ) 
		System.out.println(this+" closing !");
	    close();
	} else {
	    // Notify the server that a new connection is available:
	    ((HttpBasicServer) server).registerConnection(this);
	}
    }

    /**
     * Some data available on input, while writing to the server.
     * This callback gets called when the client is emitting data to the
     * server and the server has sent us something before we actually sent 
     * all our bytes.
     * <p>Take any appropriate action.
     */

    public void notifyInputAvailable(InputStream in) {
	return;
    }

    /**
     * Get the MIME parser to read from this connection.
     * All access to the connection's input stream should go through the MIME
     * parser to ensure buffering coherency.
     * @return A MimeParser instance suitable to parse the reply input stream.
     * @exception RuntimeException If the connection was not connected.
     */

    public MimeParser getParser() {
	if ( parser == null )
	    throw new RuntimeException("getParser while disconnected.");
	return parser;
    }

    /**
     * Get the connection output stream.
     * @return The output stream to send data on this connection.
     * @exception RuntimeException If the connection was not previously opened.
     */

    public OutputStream getOutputStream() {
	if ( output == null )
	    throw new RuntimeException("getOutputStream while disconnected.");
	return output;
    }

    /**
     * Create a new connection.
     * To be used only by HttpServer instances.
     */

    HttpBasicConnection(HttpServer server
			, int id
			, InetAddress addr
			, int port
			, int timeout
			, MimeParserFactory reply_factory)
	throws IOException
    {
	this(server, id, addr, port, timeout, 1000, reply_factory);
    }

    /**
     * Create a new connection.
     * To be used only by HttpServer instances.
     */

    HttpBasicConnection(HttpServer server
			, int id
			, InetAddress addr
			, int port
			, int timeout
			, int connect_timeout
			, MimeParserFactory reply_factory)
	throws IOException
    {
	this.server             = server;
	this.inetaddr           = addr;
	this.port               = port;
	this.id                 = id;
	this.timeout            = timeout;
	this.connect_timeout = connect_timeout;
	this.reply_factory      = reply_factory;
    }

}


