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 }