Mercurial > hg > blitz_stable
comparison src/com/go/trove/net/HttpClient.java @ 0:3dc0c5604566
Initial checkin of blitz 2.0 fcs - no installer yet.
author | Dan Creswell <dan.creswell@gmail.com> |
---|---|
date | Sat, 21 Mar 2009 11:00:06 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:3dc0c5604566 |
---|---|
1 /* ==================================================================== | |
2 * Trove - Copyright (c) 1997-2000 Walt Disney Internet Group | |
3 * ==================================================================== | |
4 * The Tea Software License, Version 1.1 | |
5 * | |
6 * Copyright (c) 2000 Walt Disney Internet Group. All rights reserved. | |
7 * | |
8 * Redistribution and use in source and binary forms, with or without | |
9 * modification, are permitted provided that the following conditions | |
10 * are met: | |
11 * | |
12 * 1. Redistributions of source code must retain the above copyright | |
13 * notice, this list of conditions and the following disclaimer. | |
14 * | |
15 * 2. Redistributions in binary form must reproduce the above copyright | |
16 * notice, this list of conditions and the following disclaimer in | |
17 * the documentation and/or other materials provided with the | |
18 * distribution. | |
19 * | |
20 * 3. The end-user documentation included with the redistribution, | |
21 * if any, must include the following acknowledgment: | |
22 * "This product includes software developed by the | |
23 * Walt Disney Internet Group (http://opensource.go.com/)." | |
24 * Alternately, this acknowledgment may appear in the software itself, | |
25 * if and wherever such third-party acknowledgments normally appear. | |
26 * | |
27 * 4. The names "Tea", "TeaServlet", "Kettle", "Trove" and "BeanDoc" must | |
28 * not be used to endorse or promote products derived from this | |
29 * software without prior written permission. For written | |
30 * permission, please contact opensource@dig.com. | |
31 * | |
32 * 5. Products derived from this software may not be called "Tea", | |
33 * "TeaServlet", "Kettle" or "Trove", nor may "Tea", "TeaServlet", | |
34 * "Kettle", "Trove" or "BeanDoc" appear in their name, without prior | |
35 * written permission of the Walt Disney Internet Group. | |
36 * | |
37 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED | |
38 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |
39 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
40 * DISCLAIMED. IN NO EVENT SHALL THE WALT DISNEY INTERNET GROUP OR ITS | |
41 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
42 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
43 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
44 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
45 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
46 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
47 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
48 * ==================================================================== | |
49 * | |
50 * For more information about Tea, please see http://opensource.go.com/. | |
51 */ | |
52 | |
53 package com.go.trove.net; | |
54 | |
55 import java.net.*; | |
56 import java.io.*; | |
57 import java.util.*; | |
58 import com.go.trove.io.*; | |
59 | |
60 /****************************************************************************** | |
61 * | |
62 * @author Brian S O'Neill | |
63 * @version | |
64 * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 01/06/14 <!-- $--> | |
65 */ | |
66 public class HttpClient { | |
67 private final SocketFactory mFactory; | |
68 private final int mReadTimeout; | |
69 | |
70 private String mMethod = "GET"; | |
71 private String mURI = ""; | |
72 private String mProtocol = "HTTP/1.0"; | |
73 private HttpHeaderMap mHeaders; | |
74 | |
75 private Object mSession; | |
76 | |
77 /** | |
78 * Constructs a HttpClient with a read timeout that matches the given | |
79 * factory's connect timeout. | |
80 * | |
81 * @param factory source of socket connections | |
82 */ | |
83 public HttpClient(SocketFactory factory) { | |
84 this(factory, factory.getDefaultTimeout()); | |
85 } | |
86 | |
87 /** | |
88 * @param factory source of socket connections | |
89 * @param readTimeout timeout on socket read operations before throwing a | |
90 * InterruptedIOException | |
91 */ | |
92 public HttpClient(SocketFactory factory, long readTimeout) { | |
93 mFactory = factory; | |
94 if (readTimeout == 0) { | |
95 mReadTimeout = 1; | |
96 } | |
97 else if (readTimeout < 0) { | |
98 mReadTimeout = 0; | |
99 } | |
100 else if (readTimeout > Integer.MAX_VALUE) { | |
101 mReadTimeout = Integer.MAX_VALUE; | |
102 } | |
103 else { | |
104 mReadTimeout = (int)readTimeout; | |
105 } | |
106 } | |
107 | |
108 /** | |
109 * Set the HTTP request method, which defaults to "GET". | |
110 * | |
111 * @return 'this', so that addtional calls may be chained together | |
112 */ | |
113 public HttpClient setMethod(String method) { | |
114 mMethod = method; | |
115 return this; | |
116 } | |
117 | |
118 /** | |
119 * Set the URI to request, which can include a query string. | |
120 * | |
121 * @return 'this', so that addtional calls may be chained together | |
122 */ | |
123 public HttpClient setURI(String uri) { | |
124 mURI = uri; | |
125 return this; | |
126 } | |
127 | |
128 /** | |
129 * Set the HTTP protocol string, which defaults to "HTTP/1.0". | |
130 * | |
131 * @return 'this', so that addtional calls may be chained together | |
132 */ | |
133 public HttpClient setProtocol(String protocol) { | |
134 mProtocol = protocol; | |
135 return this; | |
136 } | |
137 | |
138 /** | |
139 * Set a header name-value pair to the request. | |
140 * | |
141 * @return 'this', so that addtional calls may be chained together | |
142 */ | |
143 public HttpClient setHeader(String name, Object value) { | |
144 if (mHeaders == null) { | |
145 mHeaders = new HttpHeaderMap(); | |
146 } | |
147 mHeaders.put(name, value); | |
148 return this; | |
149 } | |
150 | |
151 /** | |
152 * Add a header name-value pair to the request in order for multiple values | |
153 * to be specified. | |
154 * | |
155 * @return 'this', so that addtional calls may be chained together | |
156 */ | |
157 public HttpClient addHeader(String name, Object value) { | |
158 if (mHeaders == null) { | |
159 mHeaders = new HttpHeaderMap(); | |
160 } | |
161 mHeaders.add(name, value); | |
162 return this; | |
163 } | |
164 | |
165 /** | |
166 * Set all the headers for this request, replacing any existing headers. | |
167 * If any more headers are added to this request, they will be stored in | |
168 * the given HttpHeaderMap. | |
169 * | |
170 * @return 'this', so that addtional calls may be chained together | |
171 */ | |
172 public HttpClient setHeaders(HttpHeaderMap headers) { | |
173 mHeaders = headers; | |
174 return this; | |
175 } | |
176 | |
177 /** | |
178 * Convenience method for setting the "Connection" header to "Keep-Alive" | |
179 * or "Close". | |
180 * | |
181 * @param b true for persistent connection | |
182 * @return 'this', so that addtional calls may be chained together | |
183 */ | |
184 public HttpClient setPersistent(boolean b) { | |
185 if (b) { | |
186 setHeader("Connection", "Keep-Alive"); | |
187 } | |
188 else { | |
189 setHeader("Connection", "Close"); | |
190 } | |
191 return this; | |
192 } | |
193 | |
194 /** | |
195 * Convenience method for preparing a post to the server. This method sets | |
196 * the method to "POST", sets the "Content-Length" header, and sets the | |
197 * "Content-Type" header to "application/x-www-form-urlencoded". When | |
198 * calling getResponse, PostData must be provided. | |
199 * | |
200 * @param contentLength number of bytes to be posted | |
201 * @return 'this', so that addtional calls may be chained together | |
202 */ | |
203 public HttpClient preparePost(int contentLength) { | |
204 setMethod("POST"); | |
205 setHeader("Content-Type", "application/x-www-form-urlencoded"); | |
206 setHeader("Content-Length", new Integer(contentLength)); | |
207 return this; | |
208 } | |
209 | |
210 /** | |
211 * Optionally specify a session for getting connections. If SocketFactory | |
212 * is distributed, then session helps to ensure the same server is routed | |
213 * to on multiple requests. | |
214 * | |
215 * @param session Object whose hashcode might be used to select a specific | |
216 * connection if factory is distributed. If null, then no session is used. | |
217 * @return 'this', so that addtional calls may be chained together | |
218 */ | |
219 public HttpClient setSession(Object session) { | |
220 mSession = session; | |
221 return this; | |
222 } | |
223 | |
224 /** | |
225 * Opens a connection, passes on the current request settings, and returns | |
226 * the server's response. | |
227 */ | |
228 public Response getResponse() throws ConnectException, SocketException { | |
229 return getResponse(null); | |
230 } | |
231 | |
232 /** | |
233 * Opens a connection, passes on the current request settings, and returns | |
234 * the server's response. The optional PostData parameter is used to | |
235 * supply post data to the server. The Content-Length header specifies | |
236 * how much data will be read from the PostData InputStream. If it is not | |
237 * specified, data will be read from the InputStream until EOF is reached. | |
238 * | |
239 * @param postData additional data to supply to the server, if request | |
240 * method is POST | |
241 */ | |
242 public Response getResponse(PostData postData) | |
243 throws ConnectException, SocketException | |
244 { | |
245 CheckedSocket socket = mFactory.getSocket(mSession); | |
246 socket.setSoTimeout(mReadTimeout); | |
247 | |
248 try { | |
249 CharToByteBuffer buffer = new FastCharToByteBuffer | |
250 (new DefaultByteBuffer(), "8859_1"); | |
251 buffer = new InternedCharToByteBuffer(buffer); | |
252 | |
253 buffer.append(mMethod); | |
254 buffer.append(' '); | |
255 buffer.append(mURI); | |
256 buffer.append(' '); | |
257 buffer.append(mProtocol); | |
258 buffer.append("\r\n"); | |
259 if (mHeaders != null) { | |
260 mHeaders.appendTo(buffer); | |
261 } | |
262 buffer.append("\r\n"); | |
263 | |
264 OutputStream out; | |
265 InputStream in; | |
266 | |
267 out = new FastBufferedOutputStream(socket.getOutputStream()); | |
268 buffer.writeTo(out); | |
269 if (postData != null) { | |
270 writePostData(out, postData); | |
271 } | |
272 out.flush(); | |
273 in = new FastBufferedInputStream(socket.getInputStream()); | |
274 | |
275 // Read first line to see if connection is working. | |
276 char[] buf = new char[100]; | |
277 String line; | |
278 try { | |
279 line = HttpUtils.readLine(in, buf); | |
280 } | |
281 catch (IOException e) { | |
282 line = null; | |
283 } | |
284 | |
285 if (line == null) { | |
286 // Try again with new connection. | |
287 try { | |
288 socket.close(); | |
289 } | |
290 catch (IOException e) { | |
291 } | |
292 | |
293 socket = mFactory.createSocket(mSession); | |
294 socket.setSoTimeout(mReadTimeout); | |
295 | |
296 out = new FastBufferedOutputStream(socket.getOutputStream()); | |
297 buffer.writeTo(out); | |
298 if (postData != null) { | |
299 writePostData(out, postData); | |
300 } | |
301 out.flush(); | |
302 in = new FastBufferedInputStream(socket.getInputStream()); | |
303 | |
304 // Read first line again. | |
305 if ((line = HttpUtils.readLine(in, buf)) == null) { | |
306 throw new ConnectException("No response from server"); | |
307 } | |
308 } | |
309 | |
310 return new Response(socket, mMethod, in, buf, line); | |
311 } | |
312 catch (SocketException e) { | |
313 throw e; | |
314 } | |
315 catch (InterruptedIOException e) { | |
316 throw new ConnectException("Read timeout expired: " + | |
317 mReadTimeout + ", " + e); | |
318 } | |
319 catch (IOException e) { | |
320 throw new SocketException(e.toString()); | |
321 } | |
322 } | |
323 | |
324 private void writePostData(OutputStream out, PostData postData) | |
325 throws IOException | |
326 { | |
327 InputStream in = postData.getInputStream(); | |
328 | |
329 int contentLength = -1; | |
330 if (mHeaders != null) { | |
331 Integer i = mHeaders.getInteger("Content-Length"); | |
332 if (i != null) { | |
333 contentLength = i.intValue(); | |
334 } | |
335 } | |
336 | |
337 byte[] buf; | |
338 if (contentLength < 0 || contentLength > 4000) { | |
339 buf = new byte[4000]; | |
340 } | |
341 else { | |
342 buf = new byte[contentLength]; | |
343 } | |
344 | |
345 try { | |
346 int len; | |
347 if (contentLength < 0) { | |
348 while ((len = in.read(buf)) > 0) { | |
349 out.write(buf, 0, len); | |
350 } | |
351 } | |
352 else { | |
353 while (contentLength > 0) { | |
354 len = buf.length; | |
355 if (contentLength < len) { | |
356 len = contentLength; | |
357 } | |
358 if ((len = in.read(buf, 0, len)) <= 0) { | |
359 break; | |
360 } | |
361 out.write(buf, 0, len); | |
362 contentLength -= len; | |
363 } | |
364 } | |
365 } | |
366 finally { | |
367 in.close(); | |
368 } | |
369 } | |
370 | |
371 /** | |
372 * A factory for supplying data to be written to server in a POST request. | |
373 */ | |
374 public static interface PostData { | |
375 /** | |
376 * Returns the actual data via an InputStream. If the client needs to | |
377 * reconnect to the server, this method may be called again. The | |
378 * InputStream is closed when all the post data has been read from it. | |
379 */ | |
380 public InputStream getInputStream() throws IOException; | |
381 } | |
382 | |
383 public class Response { | |
384 private final int mStatusCode; | |
385 private final String mStatusMessage; | |
386 private final HttpHeaderMap mHeaders; | |
387 | |
388 private InputStream mIn; | |
389 | |
390 Response(CheckedSocket socket, String method, | |
391 InputStream in, char[] buf, String line) throws IOException | |
392 { | |
393 int statusCode = -1; | |
394 String statusMessage = ""; | |
395 | |
396 int space = line.indexOf(' '); | |
397 if (space > 0) { | |
398 int nextSpace = line.indexOf(' ', space + 1); | |
399 String sub; | |
400 if (nextSpace < 0) { | |
401 sub = line.substring(space + 1); | |
402 } | |
403 else { | |
404 sub = line.substring(space + 1, nextSpace); | |
405 statusMessage = line.substring(nextSpace + 1); | |
406 } | |
407 try { | |
408 statusCode = Integer.parseInt(sub); | |
409 } | |
410 catch (NumberFormatException e) { | |
411 } | |
412 } | |
413 | |
414 if (statusCode < 0) { | |
415 throw new ProtocolException("Invalid HTTP response: " + line); | |
416 } | |
417 | |
418 mStatusCode = statusCode; | |
419 mStatusMessage = statusMessage; | |
420 mHeaders = new HttpHeaderMap(); | |
421 mHeaders.readFrom(in, buf); | |
422 | |
423 // Used for controlling persistent connections. | |
424 int contentLength; | |
425 if ("Keep-Alive".equalsIgnoreCase | |
426 (mHeaders.getString("Connection"))) { | |
427 | |
428 if ("HEAD".equals(method)) { | |
429 contentLength = 0; | |
430 } | |
431 else { | |
432 Integer i = mHeaders.getInteger("Content-Length"); | |
433 if (i != null) { | |
434 contentLength = i.intValue(); | |
435 } | |
436 else { | |
437 contentLength = -1; | |
438 } | |
439 } | |
440 } | |
441 else { | |
442 contentLength = -1; | |
443 } | |
444 | |
445 mIn = new ResponseInput(socket, in, contentLength); | |
446 } | |
447 | |
448 /** | |
449 * Resturns the server's status code, 200 for OK, 404 for not found, | |
450 * etc. | |
451 */ | |
452 public int getStatusCode() { | |
453 return mStatusCode; | |
454 } | |
455 | |
456 /** | |
457 * Returns the server's status message accompanying the status code. | |
458 * This message is intended for humans only. | |
459 */ | |
460 public String getStatusMessage() { | |
461 return mStatusMessage; | |
462 } | |
463 | |
464 public HttpHeaderMap getHeaders() { | |
465 return mHeaders; | |
466 } | |
467 | |
468 /** | |
469 * Returns an InputStream supplying the body of the response. When all | |
470 * of the response body has been read, the connection is either closed | |
471 * or recycled, depending on if all the criteria is met for supporting | |
472 * persistent connections. Further reads on the InputStream will | |
473 * return EOF. | |
474 */ | |
475 public InputStream getInputStream() { | |
476 return mIn; | |
477 } | |
478 } | |
479 | |
480 private class ResponseInput extends InputStream { | |
481 private CheckedSocket mSocket; | |
482 private InputStream mIn; | |
483 private int mContentLength; | |
484 | |
485 /** | |
486 * @param contentLength Used for supporting persistent connections. If | |
487 * negative, then close connection when EOF is read. | |
488 */ | |
489 public ResponseInput(CheckedSocket socket, | |
490 InputStream in, int contentLength) | |
491 throws IOException | |
492 { | |
493 mSocket = socket; | |
494 mIn = in; | |
495 if ((mContentLength = contentLength) == 0) { | |
496 recycle(); | |
497 } | |
498 } | |
499 | |
500 public int read() throws IOException { | |
501 if (mContentLength == 0) { | |
502 return -1; | |
503 } | |
504 | |
505 int b = mIn.read(); | |
506 | |
507 if (b < 0) { | |
508 close(); | |
509 } | |
510 else if (mContentLength > 0) { | |
511 if (--mContentLength == 0) { | |
512 recycle(); | |
513 } | |
514 } | |
515 | |
516 return b; | |
517 } | |
518 | |
519 public int read(byte[] b) throws IOException { | |
520 return read(b, 0, b.length); | |
521 } | |
522 | |
523 public int read(byte[] b, int off, int len) throws IOException { | |
524 if (mContentLength == 0) { | |
525 return -1; | |
526 } | |
527 | |
528 if (mContentLength < 0) { | |
529 len = mIn.read(b, off, len); | |
530 if (len < 0) { | |
531 close(); | |
532 } | |
533 else if (len == 0) { | |
534 close(); | |
535 len = -1; | |
536 } | |
537 return len; | |
538 } | |
539 | |
540 if (len > mContentLength) { | |
541 len = mContentLength; | |
542 } | |
543 else if (len == 0) { | |
544 return 0; | |
545 } | |
546 | |
547 len = mIn.read(b, off, len); | |
548 | |
549 if (len < 0) { | |
550 close(); | |
551 } | |
552 else if (len == 0) { | |
553 close(); | |
554 len = -1; | |
555 } | |
556 else { | |
557 if ((mContentLength -= len) == 0) { | |
558 recycle(); | |
559 } | |
560 } | |
561 | |
562 return len; | |
563 } | |
564 | |
565 public long skip(long n) throws IOException { | |
566 if (mContentLength == 0) { | |
567 return 0; | |
568 } | |
569 | |
570 if (mContentLength < 0) { | |
571 return mIn.skip(n); | |
572 } | |
573 | |
574 if (n > mContentLength) { | |
575 n = mContentLength; | |
576 } | |
577 else if (n == 0) { | |
578 return 0; | |
579 } | |
580 | |
581 n = mIn.skip(n); | |
582 | |
583 if ((mContentLength -= n) == 0) { | |
584 recycle(); | |
585 } | |
586 | |
587 return n; | |
588 } | |
589 | |
590 public int available() throws IOException { | |
591 return mIn.available(); | |
592 } | |
593 | |
594 public void close() throws IOException { | |
595 if (mSocket != null) { | |
596 mContentLength = 0; | |
597 mSocket = null; | |
598 mIn.close(); | |
599 } | |
600 } | |
601 | |
602 private void recycle() throws IOException { | |
603 if (mSocket != null) { | |
604 if (mContentLength == 0) { | |
605 CheckedSocket s = mSocket; | |
606 mSocket = null; | |
607 mFactory.recycleSocket(s); | |
608 } | |
609 else { | |
610 mSocket = null; | |
611 mIn.close(); | |
612 } | |
613 } | |
614 } | |
615 } | |
616 } |