/*
 * @(#)HttpOutputStream.java				0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschalär
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser 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
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/ 
 *
 */

package HTTPClient;

import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * This class provides an output stream for requests. The stream must first
 * be associated with a request before it may be used; this is done by
 * passing it to one of the request methods in HTTPConnection. Example:
 * <PRE>
 *    OutputStream out = new HttpOutputStream(12345);
 *    rsp = con.Post("/cgi-bin/my_cgi", out);
 *    out.write(...);
 *    out.close();
 *    if (rsp.getStatusCode() >= 300)
 *        ...
 * </PRE>
 *
 * <P>There are two constructors for this class, one taking a length parameter,
 * and one without any parameters. If the stream is created with a length
 * then the request will be sent with the corresponding Content-length header
 * and anything written to the stream will be written on the socket immediately.
 * This is the preferred way. If the stream is created without a length then
 * one of two things will happen: if, at the time of the request, the server
 * is known to understand HTTP/1.1 then each write() will send the data
 * immediately using the chunked encoding. If, however, either the server
 * version is unknown (because this is first request to that server) or the
 * server only understands HTTP/1.0 then all data will be written to a buffer
 * first, and only when the stream is closed will the request be sent.
 *
 * <P>Another reason that using the <var>HttpOutputStream(length)</var>
 * constructor is recommended over the <var>HttpOutputStream()</var> one is
 * that some HTTP/1.1 servers do not allow the chunked transfer encoding to
 * be used when POSTing to a cgi script. This is because the way the cgi API
 * is defined the cgi script expects a Content-length environment variable.
 * If the data is sent using the chunked transfer encoding however, then the
 * server would have to buffer all the data before invoking the cgi so that
 * this variable could be set correctly. Not all servers are willing to do
 * this.
 *
 * <P>If you cannot use the <var>HttpOutputStream(length)</var> constructor and
 * are having problems sending requests (usually a 411 response) then you can
 * try setting the system property <var>HTTPClient.dontChunkRequests</var> to
 * <var>true</var> (this needs to be done either on the command line or
 * somewhere in the code before the HTTPConnection is first accessed). This
 * will prevent the client from using the chunked encoding in this case and
 * will cause the HttpOutputStream to buffer all the data instead, sending it
 * only when close() is invoked.
 *
 * <P>The behaviour of a request sent with an output stream may differ from
 * that of a request sent with a data parameter. The reason for this is that
 * the various modules cannot resend a request which used an output stream.
 * Therefore such things as authorization and retrying of requests won't be
 * done by the HTTPClient for such requests. But see {@link
 * HTTPResponse#retryRequest() HTTPResponse.retryRequest} for a partial
 * solution.
 *
 * @version	0.3-3  06/05/2001
 * @author	Ronald Tschalär
 * @since	V0.3
 */
public class HttpOutputStream extends OutputStream
{
    /** null trailers */
    private static final NVPair[] empty = new NVPair[0];

    /** the length of the data to be sent */
    private int length;

    /** the length of the data received so far */
    private int rcvd = 0;

    /** the request this stream is associated with */
    private Request req = null;

    /** the response from sendRequest if we stalled the request */
    private Response resp = null;

    /** the socket output stream */
    private OutputStream os = null;

    /** the buffer to be used if needed */
    private ByteArrayOutputStream bos = null;

    /** the trailers to send if using chunked encoding. */
    private NVPair[] trailers = empty;

    /** the timeout to pass to SendRequest() */
    private int con_to = 0;

    /** just ignore all the data if told to do so */
    private boolean ignore = false;


    // Constructors

    /**
     * Creates an output stream of unspecified length. Note that it is
     * <strong>highly</strong> recommended that this constructor be avoided
     * where possible and <code>HttpOutputStream(int)</code> used instead.
     *
     * @see HttpOutputStream#HttpOutputStream(int)
     */
    public HttpOutputStream()
    {
	length = -1;
    }


    /**
     * This creates an output stream which will take <var>length</var> bytes
     * of data.
     *
     * @param length the number of bytes which will be sent over this stream
     */
    public HttpOutputStream(int length)
    {
	if (length < 0)
	   throw new IllegalArgumentException("Length must be greater equal 0");
	this.length = length;
    }


    // Methods

    /**
     * Associates this stream with a request and the actual output stream.
     * No other methods in this class may be invoked until this method has
     * been invoked by the HTTPConnection.
     *
     * @param req    the request this stream is to be associated with
     * @param os     the underlying output stream to write our data to, or null
     *               if we should write to a ByteArrayOutputStream instead.
     * @param con_to connection timeout to use in sendRequest()
     */
    void goAhead(Request req, OutputStream os, int con_to)
    {
	this.req    = req;
	this.os     = os;
	this.con_to = con_to;

	if (os == null)
	    bos = new ByteArrayOutputStream();

	Log.write(Log.CONN, "OutS:  Stream ready for writing");
	if (bos != null)
	    Log.write(Log.CONN, "OutS:  Buffering all data before sending " +
			        "request");
    }


    /**
     * Setup this stream to dump the data to the great bit-bucket in the sky.
     * This is needed for when a module handles the request directly.
     *
     * @param req the request this stream is to be associated with
     */
    void ignoreData(Request req)
    {
	this.req = req;
	ignore = true;
    }


    /**
     * Return the response we got from sendRequest(). This waits until
     * the request has actually been sent.
     *
     * @return the response returned by sendRequest()
     */
    synchronized Response getResponse()
    {
	while (resp == null)
	    try { wait(); } catch (InterruptedException ie) { }

	return resp;
    }


    /**
     * Returns the number of bytes this stream is willing to accept, or -1
     * if it is unbounded.
     *
     * @return the number of bytes
     */
    public int getLength()
    {
	return length;
    }


    /**
     * Gets the trailers which were set with <code>setTrailers()</code>.
     *
     * @return an array of header fields
     * @see #setTrailers(HTTPClient.NVPair[])
     */
    public NVPair[] getTrailers()
    {
	return trailers;
    }


    /**
     * Sets the trailers to be sent if the output is sent with the
     * chunked transfer encoding. These must be set before the output
     * stream is closed for them to be sent.
     *
     * <P>Any trailers set here <strong>should</strong> be mentioned
     * in a <var>Trailer</var> header in the request (see section 14.40
     * of draft-ietf-http-v11-spec-rev-06.txt).
     *
     * <P>This method (and its related <code>getTrailers()</code>)) are
     * in this class and not in <var>Request</var> because setting
     * trailers is something an application may want to do, not only
     * modules.
     *
     * @param trailers an array of header fields
     */
    public void setTrailers(NVPair[] trailers)
    {
	if (trailers != null)
	    this.trailers = trailers;
	else
	    this.trailers = empty;
    }


    /**
     * Reset this output stream, so it may be reused in a retried request.
     * This method may only be invoked by modules, and <strong>must</strong>
     * never be invoked by an application.
     */
    public void reset()
    {
	rcvd     = 0;
	req      = null;
	resp     = null;
	os       = null;
	bos      = null;
	con_to   = 0;
	ignore   = false;
    }


    /**
     * Writes a single byte on the stream. It is subject to the same rules
     * as <code>write(byte[], int, int)</code>.
     *
     * @param b the byte to write
     * @exception IOException if any exception is thrown by the socket
     * @see #write(byte[], int, int)
     */
    public void write(int b)  throws IOException, IllegalAccessError
    {
	byte[] tmp = { (byte) b };
	write(tmp, 0, 1);
    }


    /**
     * Writes an array of bytes on the stream. This method may not be used
     * until this stream has been passed to one of the methods in
     * HTTPConnection (i.e. until it has been associated with a request).
     *
     * @param buf an array containing the data to write
     * @param off the offset of the data whithin the buffer
     * @param len the number bytes (starting at <var>off</var>) to write
     * @exception IOException if any exception is thrown by the socket, or
     *            if writing <var>len</var> bytes would cause more bytes to
     *            be written than this stream is willing to accept.
     * @exception IllegalAccessError if this stream has not been associated
     *            with a request yet
     */
    public synchronized void write(byte[] buf, int off, int len)
	    throws IOException, IllegalAccessError
    {
	if (req == null)
	    throw new IllegalAccessError("Stream not associated with a request");

	if (ignore) return;

	if (length != -1  &&  rcvd+len > length)
	{
	    IOException ioe =
		new IOException("Tried to write too many bytes (" + (rcvd+len) +
				" > " + length + ")");
	    req.getConnection().closeDemux(ioe, false);
	    req.getConnection().outputFinished();
	    throw ioe;
	}

	try
	{
	    if (bos != null)
		bos.write(buf, off, len);
	    else if (length != -1)
		os.write(buf, off, len);
	    else
		os.write(Codecs.chunkedEncode(buf, off, len, null, false));
	}
	catch (IOException ioe)
	{
	    req.getConnection().closeDemux(ioe, true);
	    req.getConnection().outputFinished();
	    throw ioe;
	}

	rcvd += len;
    }


    /**
     * Closes the stream and causes the data to be sent if it has not already
     * been done so. This method <strong>must</strong> be invoked when all
     * data has been written.
     *
     * @exception IOException if any exception is thrown by the underlying
     *            socket, or if too few bytes were written.
     * @exception IllegalAccessError if this stream has not been associated
     *            with a request yet.
     */
    public synchronized void close()  throws IOException, IllegalAccessError
    {
	if (req == null)
	    throw new IllegalAccessError("Stream not associated with a request");

	if (ignore) return;

	if (bos != null)
	{
	    req.setData(bos.toByteArray());
	    req.setStream(null);

	    if (trailers.length > 0)
	    {
		NVPair[] hdrs = req.getHeaders();

		// remove any Trailer header field

		int len = hdrs.length;
		for (int idx=0; idx<len; idx++)
		{
		    if (hdrs[idx].getName().equalsIgnoreCase("Trailer"))
		    {
			System.arraycopy(hdrs, idx+1, hdrs, idx, len-idx-1);
			len--;
		    }
		}


		// add the trailers to the headers

		hdrs = Util.resizeArray(hdrs, len+trailers.length);
		System.arraycopy(trailers, 0, hdrs, len, trailers.length);

		req.setHeaders(hdrs);
	    }

	    Log.write(Log.CONN, "OutS:  Sending request");

	    try
		{ resp = req.getConnection().sendRequest(req, con_to); }
	    catch (ModuleException me)
		{ throw new IOException(me.toString()); }
	    notify();
	}
	else
	{
	    if (rcvd < length)
	    {
		IOException ioe =
		    new IOException("Premature close: only " + rcvd +
				    " bytes written instead of the " +
				    "expected " + length);
		req.getConnection().closeDemux(ioe, false);
		req.getConnection().outputFinished();
		throw ioe;
	    }

	    try
	    {
		if (length == -1)
		{
		    if (Log.isEnabled(Log.CONN)  &&  trailers.length > 0)
		    {
			Log.write(Log.CONN, "OutS:  Sending trailers:");
			for (int idx=0; idx<trailers.length; idx++)
			    Log.write(Log.CONN, "       " +
						trailers[idx].getName() + ": " +
						trailers[idx].getValue());
		    }

		    os.write(Codecs.chunkedEncode(null, 0, 0, trailers, true));
		}

		os.flush();

		Log.write(Log.CONN, "OutS:  All data sent");
	    }
	    catch (IOException ioe)
	    {
		req.getConnection().closeDemux(ioe, true);
		throw ioe;
	    }
	    finally
	    {
		req.getConnection().outputFinished();
	    }
	}
    }


    /**
     * produces a string describing this stream.
     *
     * @return a string containing the name and the length
     */
    public String toString()
    {
	return getClass().getName() + "[length=" + length + "]";
    }
}
