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

package HTTPClient;

import java.io.*;
import java.net.*;
import java.util.*;


/**
 * This defines the http-response class returned by the requests.
 *
 * @version	0.2 (bug fix 2)  23/03/1997
 * @author	Ronald Tschal&auml;r
 */

public class HTTPResponse
{
    /** our stream handler */
    private StreamDemultiplexor  stream_handler;

    /** our input stream from the stream demux */
    private InputStream  inp_stream;

    /** the method used in this request */
            String       method;

    /** did the request contain an entity? */
    private boolean      sent_entity;

    /** the status code returned. */
    private int          StatusCode;

    /** the reason line associated with the status code. */
    private String       ReasonLine;

    /** the HTTP version of the response. */
    private String       Version;

    /** the name and type of server. */
    private String       Server = null;

    /** the final URL of the document. */
    private URL          EffectiveURL = null;

    /** any headers which were received and do not fit in the above list. */
    private Hashtable    Headers = new Hashtable();

    /** the ContentLength of the data. */
    private int          ContentLength = -1;

    /** this indicates how the length of the entity body is determined */
	    int          cl_type = CL_HDRS;

    /** constants for above cl_type */
    static final int CL_HDRS    = 0,	// still reading/parsing headers
		     CL_0       = 1,	// no body
		     CL_CLOSE   = 2,	// by closing connection
		     CL_CONTLEN = 3,	// via the Content-Length header
		     CL_CHUNKED = 4,	// via chunked transfer encoding
		     CL_MP_BR   = 5;	// via multipart/byteranges

    /** the data (body) returned. */
    private byte[]       Data = null;

    /** signals if we have got and parsed the headers yet */
    boolean              got_headers = false;

    /** remembers any exception received while reading/parsing headers */
    private IOException  exception = null;


    // Constructors

    /**
     * Creates a new HTTPResponse and registers it with the
     * stream-demultiplexor.
     */
    HTTPResponse(String method, String[] con_hdrs,
		 ByteArrayOutputStream headers, byte[] data,
		 StreamDemultiplexor stream_handler)
	    throws IOException
    {
	this.method         = method;			// save for later
	this.stream_handler = stream_handler;
	sent_entity         = (data != null) ? true : false;
	stream_handler.register(this, con_hdrs, headers, data);	// register
	inp_stream  = stream_handler.getStream(this);	// get our stream
    }


    // Methods

    /**
     * give the status code for this request. These are grouped as follows:
     * <UL>
     *   <LI> 1xx - Informational (new in HTTP/1.1)
     *   <LI> 2xx - Success
     *   <LI> 3xx - Redirection
     *   <LI> 4xx - Client Error
     *   <LI> 5xx - Server Error
     * </UL>
     *
     * @exception IOException If any exception occurs on the socket.
     */
    public final int getStatusCode()  throws IOException
    {
	if (!got_headers)  getHeaders(true);
	return StatusCode;
    }

    /**
     * give the reason line associated with the status code.
     *
     * @exception IOException If any exception occurs on the socket.
     */
    public final String getReasonLine()  throws IOException
    {
	if (!got_headers)  getHeaders(true);
	return ReasonLine;
    }

    /**
     * get the HTTP version used for the response.
     *
     * @exception IOException If any exception occurs on the socket.
     */
    public final String getVersion()  throws IOException
    {
	if (!got_headers)  getHeaders(true);
	return Version;
    }

    /**
     * Wait for either a '100 Continue' or an error.
     *
     * @return the return status.
     */
    int getContinue()  throws IOException
    {
	getHeaders(false);
	return StatusCode;
    }

    /**
     * get the name and type of server.
     *
     * @exception IOException If any exception occurs on the socket.
     */
    public final String getServer()  throws IOException
    {
	if (!got_headers)  getHeaders(true);
	return Server;
    }

    /**
     * get the final URL of the document. This is set if the original
     * request was deferred via the "moved" (301, 302, or 303) return
     * status.
     *
     * @exception IOException If any exception occurs on the socket.
     */
    public final URL getEffectiveURL()  throws IOException
    {
	if (!got_headers)  getHeaders(true);
	return EffectiveURL;
    }

    /**
     * set the final URL of the document. This is only for internal use.
     */
    void setEffectiveURL(URL final_url)
    {
	EffectiveURL = final_url;
    }

    /**
     * retrieves the field for a given header.
     *
     * @param  hdr the header name.
     * @return the value for the header, or null if non-existent.
     * @exception IOException If any exception occurs on the socket.
     */
    public String getHeader(String hdr)  throws IOException
    {
	if (!got_headers)  getHeaders(true);
	return (String) Headers.get(new CIString(hdr.trim()));
    }

    /**
     * retrieves the field for a given header. The value is parsed as an
     * int.
     *
     * @param  hdr the header name.
     * @return the value for the header, or -1 if non-existent.
     * @exception IOException If any exception occurs on the socket.
     */
    public int getHeaderAsInt(String hdr)  throws IOException
    {
	String raw_num = getHeader(hdr);
	int    num = -1;

	try
	    { num = Integer.parseInt(raw_num); }
	//catch (NumberFormatException nfe)
	//    { }
	catch (NullPointerException npe)
	    { }

	return num;
    }

    /**
     * retrieves the field for a given header. The value is parsed as a
     * date.
     * <br>Note: When sending dates use .toGMTString() .
     *
     * @param  hdr the header name.
     * @return the value for the header, or null if non-existent.
     * @exception IOException If any exception occurs on the socket.
     */
    public Date getHeaderAsDate(String hdr)  throws IOException
    {
	String raw_date = getHeader(hdr);
	Date   date = null;

	try
	    { date = new Date(raw_date); }
	catch (IllegalArgumentException iae)
	{
	    long time;
	    try
		{ time = Long.parseLong(raw_date); }
	    catch (NumberFormatException nfe)
		{ time = 0; }
	    if (time < 0)  time = 0;
	    date = new Date(time);
	}
	catch (NullPointerException npe)
	    { }

	return date;
    }

    /**
     * returns an enumeration of all the headers available via getHeader().
     *
     * @exception IOException If any exception occurs on the socket.
     */
    public Enumeration listHeaders()  throws IOException
    {
	if (!got_headers)  getHeaders(true);
	return new HeaderEnumerator(Headers);
    }

    /**
     * Reads all the response data into a byte array. Note that this method
     * won't return until <em>all</em> the data has been received (so for
     * instance don't invoke this method if the server is doing a server
     * push). If getInputStream() had been previously called then this method
     * only returns any unread data remaining on the stream and then closes
     * it.
     *
     * @see #getInputStream()
     * @return an array containing the data (body) returned. If no data
     *         was returned then it's set to a zero-length array.
     * @exception IOException If any io exception occured while reading
     *			      the data
     */
    public synchronized byte[] getData()  throws IOException
    {
	if (!got_headers)  getHeaders(true);

	if (Data == null)
	{
	    try
		{ readResponseData(inp_stream); }
	    catch (IOException ioe)
		{ if (HTTPConnection.debug) ioe.printStackTrace(); throw ioe; }
	    finally
		{ inp_stream.close(); }
	}

	return Data;
    }

    /**
     * Gets an input stream from which the returned data can be read. Note
     * that if getData() had been previously called it will actually return
     * a ByteArrayInputStream created from that data.
     *
     * @see #getData()
     * @return the InputStream.
     * @exception IOException If any exception occurs on the socket.
     */
    public synchronized InputStream getInputStream()  throws IOException
    {
	if (!got_headers)  getHeaders(true);

	if (Data == null)
	    return inp_stream;
	else
	    return new ByteArrayInputStream(Data);
    }


    /**
     * produces a full list of headers and their values, one per line.
     *
     * @return a string containing the headers
     * @exception IOException if an exception occurs while reading the headers.
     */
    public String toString()
    {
	if (!got_headers)
	{
	    try
		{ getHeaders(true); }
	    catch (IOException ioe)
		{ return "Failed to read headers: " + ioe; }
	}

	String str = Version + " " + StatusCode + " " + ReasonLine + "\n";

	if (Server != null)
	    str += "Server: " + Server + "\n";
	if (EffectiveURL != null)
	    str += "Effective-URL: " + EffectiveURL + "\n";

	Enumeration hdr_list = Headers.keys();
	while (hdr_list.hasMoreElements())
	{
	    CIString hdr = (CIString) hdr_list.nextElement();
	    str += hdr.getString() + ": " + Headers.get(hdr) + "\n";
	}

	return str;
    }


    // Helper Methods

    /**
     * Gets and parses the headers. Sets up Data if no data will be received.
     *
     * @param skip_cont  if true skips over '100 Continue' status codes.
     * @exception IOException If any exception occurs while reading the headers.
     */
    private synchronized void getHeaders(boolean skip_cont)  throws IOException
    {
	String hdr, headers;

	if (got_headers)  return;
	if (exception != null)  throw new IOException(exception.getMessage());

	try
	{
	    do
	    {
		headers = readResponseHeaders(inp_stream);
		parseResponseHeaders(headers);
	    } while (StatusCode == 100  &&  skip_cont);  // Continue
	}
	catch (IOException ioe)
	{
	    exception = ioe;
	    throw ioe;
	}

	if (StatusCode == 100) return;
	got_headers = true;

	if (method.equalsIgnoreCase("HEAD")  ||  ContentLength == 0  ||
	    StatusCode <= 199  ||
	    StatusCode == 204  ||  StatusCode == 205  || StatusCode == 304)
	{
	    Data = new byte[0];		// we will not receive any more data
	    cl_type = CL_0;
	    inp_stream.close();
	}
	else if (Version.equals("HTTP/0.9"))
	{
	    cl_type = CL_CLOSE;
	    try				// make life simple
		{ readResponseData(inp_stream); }
	    finally
		{ inp_stream.close(); }
	}
	else if ((hdr = getHeader("Transfer-Encoding")) != null  &&
		 hdr.toLowerCase().indexOf("chunked") != -1)
	    cl_type = CL_CHUNKED;
	else if (ContentLength != -1)
	    cl_type = CL_CONTLEN;
	else if ((hdr = getHeader("Content-Type")) != null  &&
		 hdr.toLowerCase().indexOf("multipart/byteranges") != -1)
	    cl_type = CL_MP_BR;
	else
	{
	    cl_type = CL_CLOSE;
	    stream_handler.markForClose();
	}

	if ((hdr = getHeader("Connection")) != null  &&
	    hdr.toLowerCase().indexOf("close") != -1)
	    stream_handler.markForClose();

	if (HTTPConnection.debug)
	{
	    System.err.println("Response entity delimiter: " +
		(cl_type == CL_0       ? "No Entity"      :
		 cl_type == CL_CLOSE   ? "Close"          :
		 cl_type == CL_CONTLEN ? "Content-Length" :
		 cl_type == CL_CHUNKED ? "Chunked"        :
		 cl_type == CL_MP_BR   ? "Multipart"      :
		 "???" ) + " (" + inp_stream.hashCode() + ")");
	}
    }


    /**
     * Reads the response headers received, folding continued lines.
     *
     * @inp    the input stream from which to read the response
     * @return a (newline separated) list of headers
     * @exception IOException if any read on the input stream fails
     */
    private String readResponseHeaders(InputStream inp)
	    throws IOException
    {
	StringBuffer    hdrs   = new StringBuffer(200);
	String          line;
	byte[]          buf    = new byte[7];
	DataInputStream datain = new DataInputStream(inp);


	if (HTTPConnection.debug)
	    System.err.println("Reading Response headers (" +
				inp_stream.hashCode() + ")");

	// read 7 bytes to see type of response
	try
	{
	    while (Character.isSpace( (char) (buf[0] = datain.readByte()) ) );
	    datain.readFully(buf, 1, buf.length-1);
	}
	catch (EOFException eof)
	    { throw eof; }
	for (int idx=0; idx<buf.length; idx++)
	    hdrs.append((char) buf[idx]);

	if (hdrs.toString().equalsIgnoreCase("HTTP/1.")  || // It's 1.x
	    hdrs.toString().startsWith("HTTP "))      // NCSA bug
	{
	    while ((line = datain.readLine()) != null  &&  line.length() != 0)
	    {
		int ch = line.charAt(0);
		if (ch == ' '  ||  ch == '\t')		// a continued line
		    // replace previous \n with SP
		    hdrs.setCharAt(hdrs.length()-1, ' ');

		hdrs.append(line.trim());
		hdrs.append('\n');
	    }

	    if (line == null)
		throw new IOException("Encountered premature EOF while reading headers");
	}

	return hdrs.toString();
    }

    /**
     * Parses the headers received into a new HTTPResponse structure.
     *
     * @param  headers a (newline separated) list of headers
     * @exception ProtocolException if any part of the headers do not
     *            conform
     */
    private void parseResponseHeaders(String headers) throws ProtocolException
    {
	String          sts_line = null;
	StringTokenizer lines = new StringTokenizer(headers, "\r\n"),
			elem;


	if (HTTPConnection.debug)
	    System.err.println("Parsing Response headers:\n"+headers);


	// Detect and handle HTTP/0.9 responses

	if (!headers.regionMatches(true, 0, "HTTP/", 0, 5)  &&
	    !headers.regionMatches(true, 0, "HTTP ", 0, 5))	// NCSA bug
	{
	    Version    = "HTTP/0.9";
	    StatusCode = 200;
	    ReasonLine = "OK";

	    Data       = new byte[headers.length()];
	    headers.getBytes(0, headers.length(), Data, 0);

	    return;
	}


	// Detect and handle non HTTP/1.* responses

	if (!headers.regionMatches(true, 0, "HTTP/1.", 0, 7)  &&
	    !headers.regionMatches(true, 0, "HTTP ", 0, 5))	// NCSA bug
	    throw new ProtocolException("Received non HTTP/1.* response: " +
				    headers.substring(headers.indexOf(' ')));


	// get the status line

	try
	{
	    sts_line = lines.nextToken();
	    elem     = new StringTokenizer(sts_line, " \t");

	    Version    = elem.nextToken();
	    StatusCode = Integer.valueOf(elem.nextToken()).intValue();
	}
	catch (NoSuchElementException e)
	{
	    throw new ProtocolException("Invalid HTTP status line received: " +
					sts_line);
	}
	try
	    { ReasonLine = elem.nextToken("").trim(); }
	catch (NoSuchElementException e)
	    { ReasonLine = ""; }


	/* If the status code shows an error and we're sending (or have sent)
	 * an entity and it's length is delimited by a Content-length header,
	 * then we must close the the connection (if indeed it hasn't already
	 * been done) - RFC-2068, Section 8.2 .
	 */

	if (StatusCode >= 300  &&  sent_entity)
	    stream_handler.markForClose();


	// get the rest of the headers

	while (lines.hasMoreTokens())
	{
	    String hdr = lines.nextToken();
	    int    sep = hdr.indexOf(':');
	    if (sep == -1)
	    {
		throw new ProtocolException("Invalid HTTP header received: " +
					    hdr);
	    }

	    String hdr_name   = hdr.substring(0, sep).trim();
	    String hdr_value  = hdr.substring(sep+1).trim();

	    if (hdr_name.equalsIgnoreCase("Server"))
		Server = hdr_value;

	    else if (hdr_name.equalsIgnoreCase("Content-length"))
	    {
		ContentLength = Integer.valueOf(hdr_value).intValue();
		Headers.put(new CIString(hdr_name), hdr_value);
	    }

	    else
	    {
		if (hdr_name.equalsIgnoreCase("Location")  &&
		     (StatusCode >= 301  &&  StatusCode <= 303  ||
		      StatusCode == 305))
		{
		    try
		    {
			EffectiveURL = new URL(hdr_value);
		    }
		    catch (MalformedURLException e)
		    {
			throw new ProtocolException("Malformed URL in Location header received: "+hdr_value);
		    }
		}

		CIString ciname = new CIString(hdr_name);
		String   value  = (String) Headers.get(ciname);
		if (value == null)
		    Headers.put(ciname, hdr_value);
		else
		    Headers.put(ciname, value + "," + hdr_value);
	    }
	}
    }


    /**
     * Reads the response data received. Does not return until either
     * Content-Length bytes have been read or EOF is reached.
     *
     * @inp       the input stream from which to read the data
     * @exception IOException if any read on the input stream fails
     */
    private void readResponseData(InputStream inp) throws IOException
    {
	if (Data == null)
	    Data = new byte[0];


	// read response data

	if (ContentLength != -1)
	{
	    int rcvd = 0,
		size = 0;
	    Data = new byte[ContentLength];

	    do
	    {
		size += rcvd;
		rcvd  = inp.read(Data, size, ContentLength-size);
	    } while (size < ContentLength  &&  rcvd != -1);

	    if (rcvd == -1)	// premature EOF
		Data  = Util.resizeArray(Data, size);
	}
	else
	{
	    int inc  = 1000,
		off  = Data.length,
		rcvd = 0;

	    do
	    {
		off  += rcvd;
		Data  = Util.resizeArray(Data, off+inc);
	    } while ((rcvd = inp.read(Data, off, inc)) != -1);

	    Data = Util.resizeArray(Data, off);
	}
    }

}


/**
 * This class' raison d'etre is that I want to use a Hashtable using
 * Strings as keys and I want the lookup be case insensitive, but I
 * also want to be able retrieve the keys with original case (otherwise
 * I could just use toLowerCase() in the get() and put()). Since the
 * class String is final we create a new class that holds the string
 * and overrides the methods hashCode() and equals().
 */

final class CIString
{
    /** the string */
    private String string;

    /** the constructor */
    public CIString(String string)
    {
	this.string = string;
    }

    /** return the original string */
    public final String getString()
    {
	return string;
    }

    /** 
     * We smash case before calculation so that the hash code is
     * "case insensitive".
     */
    public int hashCode()
    {
	return string.toLowerCase().hashCode();
    }

    /**
     * Uses the case insensitive comparison.
     */
    public boolean equals(Object obj)
    {
	if ((obj != null) && (obj instanceof CIString))
	{
	    CIString str = (CIString) obj;
	    return string.equalsIgnoreCase(str.getString());
	}
	return false;
    }
}


/**
 * Produces an enumeration of the headers. Uses the hastable enumerator.
 */
final class HeaderEnumerator implements Enumeration
{
    /** use the hashtable enumerator */
    private Enumeration Headers;


    // Constructors

    HeaderEnumerator(Hashtable headers)
    {
	Headers = headers.keys();
    }


    // Methods

    public boolean hasMoreElements()
    {
	return Headers.hasMoreElements();
    }

    public Object nextElement()
    {
	return ((CIString) Headers.nextElement()).getString();
    }
}

