Mercurial > hg > blitz_condensed
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/com/go/trove/net/HttpClient.java Sat Mar 21 11:00:06 2009 +0000 @@ -0,0 +1,616 @@ +/* ==================================================================== + * Trove - Copyright (c) 1997-2000 Walt Disney Internet Group + * ==================================================================== + * The Tea Software License, Version 1.1 + * + * Copyright (c) 2000 Walt Disney Internet Group. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Walt Disney Internet Group (http://opensource.go.com/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Tea", "TeaServlet", "Kettle", "Trove" and "BeanDoc" must + * not be used to endorse or promote products derived from this + * software without prior written permission. For written + * permission, please contact opensource@dig.com. + * + * 5. Products derived from this software may not be called "Tea", + * "TeaServlet", "Kettle" or "Trove", nor may "Tea", "TeaServlet", + * "Kettle", "Trove" or "BeanDoc" appear in their name, without prior + * written permission of the Walt Disney Internet Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE WALT DISNEY INTERNET GROUP OR ITS + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * For more information about Tea, please see http://opensource.go.com/. + */ + +package com.go.trove.net; + +import java.net.*; +import java.io.*; +import java.util.*; +import com.go.trove.io.*; + +/****************************************************************************** + * + * @author Brian S O'Neill + * @version + * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 01/06/14 <!-- $--> + */ +public class HttpClient { + private final SocketFactory mFactory; + private final int mReadTimeout; + + private String mMethod = "GET"; + private String mURI = ""; + private String mProtocol = "HTTP/1.0"; + private HttpHeaderMap mHeaders; + + private Object mSession; + + /** + * Constructs a HttpClient with a read timeout that matches the given + * factory's connect timeout. + * + * @param factory source of socket connections + */ + public HttpClient(SocketFactory factory) { + this(factory, factory.getDefaultTimeout()); + } + + /** + * @param factory source of socket connections + * @param readTimeout timeout on socket read operations before throwing a + * InterruptedIOException + */ + public HttpClient(SocketFactory factory, long readTimeout) { + mFactory = factory; + if (readTimeout == 0) { + mReadTimeout = 1; + } + else if (readTimeout < 0) { + mReadTimeout = 0; + } + else if (readTimeout > Integer.MAX_VALUE) { + mReadTimeout = Integer.MAX_VALUE; + } + else { + mReadTimeout = (int)readTimeout; + } + } + + /** + * Set the HTTP request method, which defaults to "GET". + * + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient setMethod(String method) { + mMethod = method; + return this; + } + + /** + * Set the URI to request, which can include a query string. + * + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient setURI(String uri) { + mURI = uri; + return this; + } + + /** + * Set the HTTP protocol string, which defaults to "HTTP/1.0". + * + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient setProtocol(String protocol) { + mProtocol = protocol; + return this; + } + + /** + * Set a header name-value pair to the request. + * + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient setHeader(String name, Object value) { + if (mHeaders == null) { + mHeaders = new HttpHeaderMap(); + } + mHeaders.put(name, value); + return this; + } + + /** + * Add a header name-value pair to the request in order for multiple values + * to be specified. + * + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient addHeader(String name, Object value) { + if (mHeaders == null) { + mHeaders = new HttpHeaderMap(); + } + mHeaders.add(name, value); + return this; + } + + /** + * Set all the headers for this request, replacing any existing headers. + * If any more headers are added to this request, they will be stored in + * the given HttpHeaderMap. + * + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient setHeaders(HttpHeaderMap headers) { + mHeaders = headers; + return this; + } + + /** + * Convenience method for setting the "Connection" header to "Keep-Alive" + * or "Close". + * + * @param b true for persistent connection + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient setPersistent(boolean b) { + if (b) { + setHeader("Connection", "Keep-Alive"); + } + else { + setHeader("Connection", "Close"); + } + return this; + } + + /** + * Convenience method for preparing a post to the server. This method sets + * the method to "POST", sets the "Content-Length" header, and sets the + * "Content-Type" header to "application/x-www-form-urlencoded". When + * calling getResponse, PostData must be provided. + * + * @param contentLength number of bytes to be posted + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient preparePost(int contentLength) { + setMethod("POST"); + setHeader("Content-Type", "application/x-www-form-urlencoded"); + setHeader("Content-Length", new Integer(contentLength)); + return this; + } + + /** + * Optionally specify a session for getting connections. If SocketFactory + * is distributed, then session helps to ensure the same server is routed + * to on multiple requests. + * + * @param session Object whose hashcode might be used to select a specific + * connection if factory is distributed. If null, then no session is used. + * @return 'this', so that addtional calls may be chained together + */ + public HttpClient setSession(Object session) { + mSession = session; + return this; + } + + /** + * Opens a connection, passes on the current request settings, and returns + * the server's response. + */ + public Response getResponse() throws ConnectException, SocketException { + return getResponse(null); + } + + /** + * Opens a connection, passes on the current request settings, and returns + * the server's response. The optional PostData parameter is used to + * supply post data to the server. The Content-Length header specifies + * how much data will be read from the PostData InputStream. If it is not + * specified, data will be read from the InputStream until EOF is reached. + * + * @param postData additional data to supply to the server, if request + * method is POST + */ + public Response getResponse(PostData postData) + throws ConnectException, SocketException + { + CheckedSocket socket = mFactory.getSocket(mSession); + socket.setSoTimeout(mReadTimeout); + + try { + CharToByteBuffer buffer = new FastCharToByteBuffer + (new DefaultByteBuffer(), "8859_1"); + buffer = new InternedCharToByteBuffer(buffer); + + buffer.append(mMethod); + buffer.append(' '); + buffer.append(mURI); + buffer.append(' '); + buffer.append(mProtocol); + buffer.append("\r\n"); + if (mHeaders != null) { + mHeaders.appendTo(buffer); + } + buffer.append("\r\n"); + + OutputStream out; + InputStream in; + + out = new FastBufferedOutputStream(socket.getOutputStream()); + buffer.writeTo(out); + if (postData != null) { + writePostData(out, postData); + } + out.flush(); + in = new FastBufferedInputStream(socket.getInputStream()); + + // Read first line to see if connection is working. + char[] buf = new char[100]; + String line; + try { + line = HttpUtils.readLine(in, buf); + } + catch (IOException e) { + line = null; + } + + if (line == null) { + // Try again with new connection. + try { + socket.close(); + } + catch (IOException e) { + } + + socket = mFactory.createSocket(mSession); + socket.setSoTimeout(mReadTimeout); + + out = new FastBufferedOutputStream(socket.getOutputStream()); + buffer.writeTo(out); + if (postData != null) { + writePostData(out, postData); + } + out.flush(); + in = new FastBufferedInputStream(socket.getInputStream()); + + // Read first line again. + if ((line = HttpUtils.readLine(in, buf)) == null) { + throw new ConnectException("No response from server"); + } + } + + return new Response(socket, mMethod, in, buf, line); + } + catch (SocketException e) { + throw e; + } + catch (InterruptedIOException e) { + throw new ConnectException("Read timeout expired: " + + mReadTimeout + ", " + e); + } + catch (IOException e) { + throw new SocketException(e.toString()); + } + } + + private void writePostData(OutputStream out, PostData postData) + throws IOException + { + InputStream in = postData.getInputStream(); + + int contentLength = -1; + if (mHeaders != null) { + Integer i = mHeaders.getInteger("Content-Length"); + if (i != null) { + contentLength = i.intValue(); + } + } + + byte[] buf; + if (contentLength < 0 || contentLength > 4000) { + buf = new byte[4000]; + } + else { + buf = new byte[contentLength]; + } + + try { + int len; + if (contentLength < 0) { + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + else { + while (contentLength > 0) { + len = buf.length; + if (contentLength < len) { + len = contentLength; + } + if ((len = in.read(buf, 0, len)) <= 0) { + break; + } + out.write(buf, 0, len); + contentLength -= len; + } + } + } + finally { + in.close(); + } + } + + /** + * A factory for supplying data to be written to server in a POST request. + */ + public static interface PostData { + /** + * Returns the actual data via an InputStream. If the client needs to + * reconnect to the server, this method may be called again. The + * InputStream is closed when all the post data has been read from it. + */ + public InputStream getInputStream() throws IOException; + } + + public class Response { + private final int mStatusCode; + private final String mStatusMessage; + private final HttpHeaderMap mHeaders; + + private InputStream mIn; + + Response(CheckedSocket socket, String method, + InputStream in, char[] buf, String line) throws IOException + { + int statusCode = -1; + String statusMessage = ""; + + int space = line.indexOf(' '); + if (space > 0) { + int nextSpace = line.indexOf(' ', space + 1); + String sub; + if (nextSpace < 0) { + sub = line.substring(space + 1); + } + else { + sub = line.substring(space + 1, nextSpace); + statusMessage = line.substring(nextSpace + 1); + } + try { + statusCode = Integer.parseInt(sub); + } + catch (NumberFormatException e) { + } + } + + if (statusCode < 0) { + throw new ProtocolException("Invalid HTTP response: " + line); + } + + mStatusCode = statusCode; + mStatusMessage = statusMessage; + mHeaders = new HttpHeaderMap(); + mHeaders.readFrom(in, buf); + + // Used for controlling persistent connections. + int contentLength; + if ("Keep-Alive".equalsIgnoreCase + (mHeaders.getString("Connection"))) { + + if ("HEAD".equals(method)) { + contentLength = 0; + } + else { + Integer i = mHeaders.getInteger("Content-Length"); + if (i != null) { + contentLength = i.intValue(); + } + else { + contentLength = -1; + } + } + } + else { + contentLength = -1; + } + + mIn = new ResponseInput(socket, in, contentLength); + } + + /** + * Resturns the server's status code, 200 for OK, 404 for not found, + * etc. + */ + public int getStatusCode() { + return mStatusCode; + } + + /** + * Returns the server's status message accompanying the status code. + * This message is intended for humans only. + */ + public String getStatusMessage() { + return mStatusMessage; + } + + public HttpHeaderMap getHeaders() { + return mHeaders; + } + + /** + * Returns an InputStream supplying the body of the response. When all + * of the response body has been read, the connection is either closed + * or recycled, depending on if all the criteria is met for supporting + * persistent connections. Further reads on the InputStream will + * return EOF. + */ + public InputStream getInputStream() { + return mIn; + } + } + + private class ResponseInput extends InputStream { + private CheckedSocket mSocket; + private InputStream mIn; + private int mContentLength; + + /** + * @param contentLength Used for supporting persistent connections. If + * negative, then close connection when EOF is read. + */ + public ResponseInput(CheckedSocket socket, + InputStream in, int contentLength) + throws IOException + { + mSocket = socket; + mIn = in; + if ((mContentLength = contentLength) == 0) { + recycle(); + } + } + + public int read() throws IOException { + if (mContentLength == 0) { + return -1; + } + + int b = mIn.read(); + + if (b < 0) { + close(); + } + else if (mContentLength > 0) { + if (--mContentLength == 0) { + recycle(); + } + } + + return b; + } + + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public int read(byte[] b, int off, int len) throws IOException { + if (mContentLength == 0) { + return -1; + } + + if (mContentLength < 0) { + len = mIn.read(b, off, len); + if (len < 0) { + close(); + } + else if (len == 0) { + close(); + len = -1; + } + return len; + } + + if (len > mContentLength) { + len = mContentLength; + } + else if (len == 0) { + return 0; + } + + len = mIn.read(b, off, len); + + if (len < 0) { + close(); + } + else if (len == 0) { + close(); + len = -1; + } + else { + if ((mContentLength -= len) == 0) { + recycle(); + } + } + + return len; + } + + public long skip(long n) throws IOException { + if (mContentLength == 0) { + return 0; + } + + if (mContentLength < 0) { + return mIn.skip(n); + } + + if (n > mContentLength) { + n = mContentLength; + } + else if (n == 0) { + return 0; + } + + n = mIn.skip(n); + + if ((mContentLength -= n) == 0) { + recycle(); + } + + return n; + } + + public int available() throws IOException { + return mIn.available(); + } + + public void close() throws IOException { + if (mSocket != null) { + mContentLength = 0; + mSocket = null; + mIn.close(); + } + } + + private void recycle() throws IOException { + if (mSocket != null) { + if (mContentLength == 0) { + CheckedSocket s = mSocket; + mSocket = null; + mFactory.recycleSocket(s); + } + else { + mSocket = null; + mIn.close(); + } + } + } + } +}