-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathHttpOutputStream.java
More file actions
456 lines (394 loc) · 13.4 KB
/
HttpOutputStream.java
File metadata and controls
456 lines (394 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
/*
* @(#)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 + "]";
}
}