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 }