/* * @(#)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: *
 *    OutputStream out = new HttpOutputStream(12345);
 *    rsp = con.Post("/cgi-bin/my_cgi", out);
 *    out.write(...);
 *    out.close();
 *    if (rsp.getStatusCode() >= 300)
 *        ...
 * 
* *

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. * *

Another reason that using the HttpOutputStream(length) * constructor is recommended over the HttpOutputStream() 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. * *

If you cannot use the HttpOutputStream(length) constructor and * are having problems sending requests (usually a 411 response) then you can * try setting the system property HTTPClient.dontChunkRequests to * true (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. * *

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 * highly recommended that this constructor be avoided * where possible and HttpOutputStream(int) used instead. * * @see HttpOutputStream#HttpOutputStream(int) */ public HttpOutputStream() { length = -1; } /** * This creates an output stream which will take length 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 setTrailers(). * * @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. * *

Any trailers set here should be mentioned * in a Trailer header in the request (see section 14.40 * of draft-ietf-http-v11-spec-rev-06.txt). * *

This method (and its related getTrailers())) are * in this class and not in Request 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 must * 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 write(byte[], int, int). * * @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 off) to write * @exception IOException if any exception is thrown by the socket, or * if writing len 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 must 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 0) { Log.write(Log.CONN, "OutS: Sending trailers:"); for (int idx=0; idx