1 /*
2 * @(#)HttpOutputStream.java 0.3-3 06/05/2001
3 *
4 * This file is part of the HTTPClient package
5 * Copyright (C) 1996-2001 Ronald Tschalär
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library; if not, write to the Free
19 * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
20 * MA 02111-1307, USA
21 *
22 * For questions, suggestions, bug-reports, enhancement-requests etc.
23 * I may be contacted at:
24 *
25 * ronald@innovation.ch
26 *
27 * The HTTPClient's home page is located at:
28 *
29 * http://www.innovation.ch/java/HTTPClient/
30 *
31 * This file contains modifications for use with "The Grinder"
32 * (http://grinder.sourceforge.net) under the terms of the LGPL. They
33 * are marked below with the comment "GRINDER MODIFICATION".
34 */
35
36 package HTTPClient;
37
38 import java.io.OutputStream;
39 import java.io.ByteArrayOutputStream;
40 import java.io.IOException;
41
42 /**
43 * This class provides an output stream for requests. The stream must first
44 * be associated with a request before it may be used; this is done by
45 * passing it to one of the request methods in HTTPConnection. Example:
46 * <PRE>
47 * OutputStream out = new HttpOutputStream(12345);
48 * rsp = con.Post("/cgi-bin/my_cgi", out);
49 * out.write(...);
50 * out.close();
51 * if (rsp.getStatusCode() >= 300)
52 * ...
53 * </PRE>
54 *
55 * <P>There are two constructors for this class, one taking a length parameter,
56 * and one without any parameters. If the stream is created with a length
57 * then the request will be sent with the corresponding Content-length header
58 * and anything written to the stream will be written on the socket immediately.
59 * This is the preferred way. If the stream is created without a length then
60 * one of two things will happen: if, at the time of the request, the server
61 * is known to understand HTTP/1.1 then each write() will send the data
62 * immediately using the chunked encoding. If, however, either the server
63 * version is unknown (because this is first request to that server) or the
64 * server only understands HTTP/1.0 then all data will be written to a buffer
65 * first, and only when the stream is closed will the request be sent.
66 *
67 * <P>Another reason that using the <var>HttpOutputStream(length)</var>
68 * constructor is recommended over the <var>HttpOutputStream()</var> one is
69 * that some HTTP/1.1 servers do not allow the chunked transfer encoding to
70 * be used when POSTing to a cgi script. This is because the way the cgi API
71 * is defined the cgi script expects a Content-length environment variable.
72 * If the data is sent using the chunked transfer encoding however, then the
73 * server would have to buffer all the data before invoking the cgi so that
74 * this variable could be set correctly. Not all servers are willing to do
75 * this.
76 *
77 * <P>If you cannot use the <var>HttpOutputStream(length)</var> constructor and
78 * are having problems sending requests (usually a 411 response) then you can
79 * try setting the system property <var>HTTPClient.dontChunkRequests</var> to
80 * <var>true</var> (this needs to be done either on the command line or
81 * somewhere in the code before the HTTPConnection is first accessed). This
82 * will prevent the client from using the chunked encoding in this case and
83 * will cause the HttpOutputStream to buffer all the data instead, sending it
84 * only when close() is invoked.
85 *
86 * <P>The behaviour of a request sent with an output stream may differ from
87 * that of a request sent with a data parameter. The reason for this is that
88 * the various modules cannot resend a request which used an output stream.
89 * Therefore such things as authorization and retrying of requests won't be
90 * done by the HTTPClient for such requests. But see {@link
91 * HTTPResponse#retryRequest() HTTPResponse.retryRequest} for a partial
92 * solution.
93 *
94 * @version 0.3-3 06/05/2001
95 * @author Ronald Tschalär
96 * @since V0.3
97 */
98 public class HttpOutputStream extends OutputStream
99 {
100 /** null trailers */
101 private static final NVPair[] empty = new NVPair[0];
102
103 /** the length of the data to be sent */
104
105 /** ++GRINDER MODIFICATION **/
106 //private int length;
107 private long length;
108
109 /** the length of the data received so far */
110 // private int rcvd = 0;
111 private long rcvd = 0;
112 /** --GRINDER MODIFICATION **/
113
114 /** the request this stream is associated with */
115 private Request req = null;
116
117 /** the response from sendRequest if we stalled the request */
118 private Response resp = null;
119
120 /** the socket output stream */
121 private OutputStream os = null;
122
123 /** the buffer to be used if needed */
124 private ByteArrayOutputStream bos = null;
125
126 /** the trailers to send if using chunked encoding. */
127 private NVPair[] trailers = empty;
128
129 /** the timeout to pass to SendRequest() */
130 private int con_to = 0;
131
132 /** just ignore all the data if told to do so */
133 private boolean ignore = false;
134
135
136 // Constructors
137
138 /**
139 * Creates an output stream of unspecified length. Note that it is
140 * <strong>highly</strong> recommended that this constructor be avoided
141 * where possible and <code>HttpOutputStream(int)</code> used instead.
142 *
143 * @see HttpOutputStream#HttpOutputStream(int)
144 */
145 public HttpOutputStream()
146 {
147 length = -1;
148 }
149
150
151 /**
152 * This creates an output stream which will take <var>length</var> bytes
153 * of data.
154 *
155 * @param length the number of bytes which will be sent over this stream
156 */
157 public HttpOutputStream(long /*GRINDER MODIFICATION int */ length)
158 {
159 if (length < 0)
160 throw new IllegalArgumentException("Length must be greater equal 0");
161 this.length = length;
162 }
163
164
165 // Methods
166
167 /**
168 * Associates this stream with a request and the actual output stream.
169 * No other methods in this class may be invoked until this method has
170 * been invoked by the HTTPConnection.
171 *
172 * @param req the request this stream is to be associated with
173 * @param os the underlying output stream to write our data to, or null
174 * if we should write to a ByteArrayOutputStream instead.
175 * @param con_to connection timeout to use in sendRequest()
176 */
177 void goAhead(Request req, OutputStream os, int con_to)
178 {
179 this.req = req;
180 this.os = os;
181 this.con_to = con_to;
182
183 if (os == null)
184 bos = new ByteArrayOutputStream();
185
186 Log.write(Log.CONN, "OutS: Stream ready for writing");
187 if (bos != null)
188 Log.write(Log.CONN, "OutS: Buffering all data before sending " +
189 "request");
190 }
191
192
193 /**
194 * Setup this stream to dump the data to the great bit-bucket in the sky.
195 * This is needed for when a module handles the request directly.
196 *
197 * @param req the request this stream is to be associated with
198 */
199 void ignoreData(Request req)
200 {
201 this.req = req;
202 ignore = true;
203 }
204
205
206 /**
207 * Return the response we got from sendRequest(). This waits until
208 * the request has actually been sent.
209 *
210 * @return the response returned by sendRequest()
211 */
212 synchronized Response getResponse()
213 {
214 while (resp == null)
215 try { wait(); } catch (InterruptedException ie) { }
216
217 return resp;
218 }
219
220
221 /**
222 * Returns the number of bytes this stream is willing to accept, or -1
223 * if it is unbounded.
224 *
225 * @return the number of bytes
226 */
227 public long /* GRINDER MODIFICICATION int */ getLength()
228 {
229 return length;
230 }
231
232
233 /**
234 * Gets the trailers which were set with <code>setTrailers()</code>.
235 *
236 * @return an array of header fields
237 * @see #setTrailers(HTTPClient.NVPair[])
238 */
239 public NVPair[] getTrailers()
240 {
241 return trailers;
242 }
243
244
245 /**
246 * Sets the trailers to be sent if the output is sent with the
247 * chunked transfer encoding. These must be set before the output
248 * stream is closed for them to be sent.
249 *
250 * <P>Any trailers set here <strong>should</strong> be mentioned
251 * in a <var>Trailer</var> header in the request (see section 14.40
252 * of draft-ietf-http-v11-spec-rev-06.txt).
253 *
254 * <P>This method (and its related <code>getTrailers()</code>)) are
255 * in this class and not in <var>Request</var> because setting
256 * trailers is something an application may want to do, not only
257 * modules.
258 *
259 * @param trailers an array of header fields
260 */
261 public void setTrailers(NVPair[] trailers)
262 {
263 if (trailers != null)
264 this.trailers = trailers;
265 else
266 this.trailers = empty;
267 }
268
269
270 /**
271 * Reset this output stream, so it may be reused in a retried request.
272 * This method may only be invoked by modules, and <strong>must</strong>
273 * never be invoked by an application.
274 */
275 public void reset()
276 {
277 rcvd = 0;
278 req = null;
279 resp = null;
280 os = null;
281 bos = null;
282 con_to = 0;
283 ignore = false;
284 }
285
286
287 /**
288 * Writes a single byte on the stream. It is subject to the same rules
289 * as <code>write(byte[], int, int)</code>.
290 *
291 * @param b the byte to write
292 * @exception IOException if any exception is thrown by the socket
293 * @see #write(byte[], int, int)
294 */
295 public void write(int b) throws IOException, IllegalAccessError
296 {
297 byte[] tmp = { (byte) b };
298 write(tmp, 0, 1);
299 }
300
301
302 /**
303 * Writes an array of bytes on the stream. This method may not be used
304 * until this stream has been passed to one of the methods in
305 * HTTPConnection (i.e. until it has been associated with a request).
306 *
307 * @param buf an array containing the data to write
308 * @param off the offset of the data whithin the buffer
309 * @param len the number bytes (starting at <var>off</var>) to write
310 * @exception IOException if any exception is thrown by the socket, or
311 * if writing <var>len</var> bytes would cause more bytes to
312 * be written than this stream is willing to accept.
313 * @exception IllegalAccessError if this stream has not been associated
314 * with a request yet
315 */
316 public synchronized void write(byte[] buf, int off, int len)
317 throws IOException, IllegalAccessError
318 {
319 if (req == null)
320 throw new IllegalAccessError("Stream not associated with a request");
321
322 if (ignore) return;
323
324 if (length != -1 && rcvd+len > length)
325 {
326 IOException ioe =
327 new IOException("Tried to write too many bytes (" + (rcvd+len) +
328 " > " + length + ")");
329 req.getConnection().closeDemux(ioe, false);
330 req.getConnection().outputFinished();
331 throw ioe;
332 }
333
334 try
335 {
336 if (bos != null)
337 bos.write(buf, off, len);
338 else if (length != -1)
339 os.write(buf, off, len);
340 else
341 os.write(Codecs.chunkedEncode(buf, off, len, null, false));
342 }
343 catch (IOException ioe)
344 {
345 req.getConnection().closeDemux(ioe, true);
346 req.getConnection().outputFinished();
347 throw ioe;
348 }
349
350 rcvd += len;
351 }
352
353
354 /**
355 * Closes the stream and causes the data to be sent if it has not already
356 * been done so. This method <strong>must</strong> be invoked when all
357 * data has been written.
358 *
359 * @exception IOException if any exception is thrown by the underlying
360 * socket, or if too few bytes were written.
361 * @exception IllegalAccessError if this stream has not been associated
362 * with a request yet.
363 */
364 public synchronized void close() throws IOException, IllegalAccessError
365 {
366 if (req == null)
367 throw new IllegalAccessError("Stream not associated with a request");
368
369 if (ignore) return;
370
371 if (bos != null)
372 {
373 req.setData(bos.toByteArray());
374 req.setStream(null);
375
376 if (trailers.length > 0)
377 {
378 NVPair[] hdrs = req.getHeaders();
379
380 // remove any Trailer header field
381
382 int len = hdrs.length;
383 for (int idx=0; idx<len; idx++)
384 {
385 if (hdrs[idx].getName().equalsIgnoreCase("Trailer"))
386 {
387 System.arraycopy(hdrs, idx+1, hdrs, idx, len-idx-1);
388 len--;
389 }
390 }
391
392
393 // add the trailers to the headers
394
395 hdrs = Util.resizeArray(hdrs, len+trailers.length);
396 System.arraycopy(trailers, 0, hdrs, len, trailers.length);
397
398 req.setHeaders(hdrs);
399 }
400
401 Log.write(Log.CONN, "OutS: Sending request");
402
403 try
404 { resp = req.getConnection().sendRequest(req, con_to); }
405 catch (ModuleException me)
406 { throw new IOException(me.toString()); }
407 notify();
408 }
409 else
410 {
411 if (rcvd < length)
412 {
413 IOException ioe =
414 new IOException("Premature close: only " + rcvd +
415 " bytes written instead of the " +
416 "expected " + length);
417 req.getConnection().closeDemux(ioe, false);
418 req.getConnection().outputFinished();
419 throw ioe;
420 }
421
422 try
423 {
424 if (length == -1)
425 {
426 if (Log.isEnabled(Log.CONN) && trailers.length > 0)
427 {
428 Log.write(Log.CONN, "OutS: Sending trailers:");
429 for (int idx=0; idx<trailers.length; idx++)
430 Log.write(Log.CONN, " " +
431 trailers[idx].getName() + ": " +
432 trailers[idx].getValue());
433 }
434
435 os.write(Codecs.chunkedEncode(null, 0, 0, trailers, true));
436 }
437
438 os.flush();
439
440 Log.write(Log.CONN, "OutS: All data sent");
441 }
442 catch (IOException ioe)
443 {
444 req.getConnection().closeDemux(ioe, true);
445 throw ioe;
446 }
447 finally
448 {
449 req.getConnection().outputFinished();
450 }
451 }
452 }
453
454
455 /**
456 * produces a string describing this stream.
457 *
458 * @return a string containing the name and the length
459 */
460 public String toString()
461 {
462 return getClass().getName() + "[length=" + length + "]";
463 }
464 }