HTTPClient.HttpOutputStream Maven / Gradle / Ivy
Show all versions of grinder-httpclient Show documentation
/*
* @(#)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:
*
* [email protected]
*
* The HTTPClient's home page is located at:
*
* http://www.innovation.ch/java/HTTPClient/
*
* This file contains modifications for use with "The Grinder"
* (http://grinder.sourceforge.net) under the terms of the LGPL. They
* are marked below with the comment "GRINDER MODIFICATION".
*/
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 */
/** ++GRINDER MODIFICATION **/
//private int length;
private long length;
/** the length of the data received so far */
// private int rcvd = 0;
private long rcvd = 0;
/** --GRINDER MODIFICATION **/
/** 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(long /*GRINDER MODIFICATION 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 long /* GRINDER MODIFICICATION 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