/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR
* 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.
*/
package com.twelvemonkeys.net;
import com.twelvemonkeys.lang.StringUtil;
import java.io.*;
import java.net.*;
import java.util.*;
/**
* A URLConnection with support for HTTP-specific features. See
* <A HREF="http://www.w3.org/pub/WWW/Protocols/">the spec</A> for details.
* This version also supports read and connect timeouts, making it more useful
* for clients with limitted time.
* <P/>
* Note that the timeouts are created on the socket level, and that
* <P/>
* Note: This class should now work as expected, but it needs more testing before
* it can enter production release.
* <BR/>
* --.k
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $
* @todo Write JUnit TestCase
* @todo ConnectionMananger!
* @see <A href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">RFC 2616</A>
*/
public class HttpURLConnection extends java.net.HttpURLConnection {
/**
* HTTP Status-Code 307: Temporary Redirect
*/
public final static int HTTP_REDIRECT = 307;
private final static int HTTP_DEFAULT_PORT = 80;
private final static String HTTP_HEADER_END = "\r\n\r\n";
private static final String HEADER_WWW_AUTH = "WWW-Authenticate";
private final static int BUF_SIZE = 8192;
private int maxRedirects = (System.getProperty("http.maxRedirects") != null)
? Integer.parseInt(System.getProperty("http.maxRedirects"))
: 20;
protected int timeout = -1;
protected int connectTimeout = -1;
private Socket socket = null;
protected InputStream errorStream = null;
protected InputStream inputStream = null;
protected OutputStream outputStream = null;
private String[] responseHeaders = null;
protected Properties responseHeaderFields = null;
protected Properties requestProperties = new Properties();
/**
* Creates a HttpURLConnection.
*
* @param pURL the URL to connect to.
*/
protected HttpURLConnection(URL pURL) {
this(pURL, 0, 0);
}
/**
* Creates a HttpURLConnection with a given read and connect timeout.
* A timeout value of zero is interpreted as an
* infinite timeout.
*
* @param pURL the URL to connect to.
* @param pTimeout the maximum time the socket will block for read
* and connect operations.
*/
protected HttpURLConnection(URL pURL, int pTimeout) {
this(pURL, pTimeout, pTimeout);
}
/**
* Creates a HttpURLConnection with a given read and connect timeout.
* A timeout value of zero is interpreted as an
* infinite timeout.
*
* @param pURL the URL to connect to.
* @param pTimeout the maximum time the socket will block for read
* operations.
* @param pConnectTimeout the maximum time the socket will block for
* connection.
*/
protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) {
super(pURL);
setTimeout(pTimeout);
connectTimeout = pConnectTimeout;
}
/**
* Sets the general request property. If a property with the key already
* exists, overwrite its value with the new value.
* <p/>
* <p> NOTE: HTTP requires all request properties which can
* legally have multiple instances with the same key
* to use a comma-seperated list syntax which enables multiple
* properties to be appended into a single property.
*
* @param pKey the keyword by which the request is known
* (e.g., "{@code accept}").
* @param pValue the value associated with it.
* @see #getRequestProperty(java.lang.String)
*/
public void setRequestProperty(String pKey, String pValue) {
if (connected) {
throw new IllegalAccessError("Already connected");
}
String oldValue = requestProperties.getProperty(pKey);
if (oldValue == null) {
requestProperties.setProperty(pKey, pValue);
}
else {
requestProperties.setProperty(pKey, oldValue + ", " + pValue);
}
}
/**
* Returns the value of the named general request property for this
* connection.
*
* @param pKey the keyword by which the request is known (e.g., "accept").
* @return the value of the named general request property for this
* connection.
* @see #setRequestProperty(java.lang.String, java.lang.String)
*/
public String getRequestProperty(String pKey) {
if (connected) {
throw new IllegalAccessError("Already connected");
}
return requestProperties.getProperty(pKey);
}
/**
* Gets HTTP response status from responses like:
* <PRE>
* HTTP/1.0 200 OK
* HTTP/1.0 401 Unauthorized
* </PRE>
* Extracts the ints 200 and 401 respectively.
* Returns -1 if none can be discerned
* from the response (i.e., the response is not valid HTTP).
* <p/>
* <!-- This is the J2SE 1.3 implementation... -->
*
* @return the HTTP Status-Code
* @throws IOException if an error occurred connecting to the server.
*/
public int getResponseCode() throws IOException {
if (responseCode != -1) {
return responseCode;
}
// Make sure we've gotten the headers
getInputStream();
String resp = getHeaderField(0);
// should have no leading/trailing LWS
// expedite the typical case by assuming it has the
// form "HTTP/1.x <WS> 2XX <mumble>"
int ind;
try {
ind = resp.indexOf(' ');
while (resp.charAt(ind) == ' ') {
ind++;
}
responseCode = Integer.parseInt(resp.substring(ind, ind + 3));
responseMessage = resp.substring(ind + 4).trim();
return responseCode;
}
catch (Exception e) {
return responseCode;
}
}
/**
* Returns the name of the specified header field.
*
* @param pName the name of a header field.
* @return the value of the named header field, or {@code null}
* if there is no such field in the header.
*/
public String getHeaderField(String pName) {
return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName));
}
/**
* Returns the value for the {@code n}<sup>th</sup> header field.
* It returns {@code null} if there are fewer than
* {@code n} fields.
* <p/>
* This method can be used in conjunction with the
* {@code getHeaderFieldKey} method to iterate through all
* the headers in the message.
*
* @param pIndex an index.
* @return the value of the {@code n}<sup>th</sup> header field.
* @see java.net.URLConnection#getHeaderFieldKey(int)
*/
public String getHeaderField(int pIndex) {
// TODO: getInputStream() first, to make sure we have header fields
if (pIndex >= responseHeaders.length) {
return null;
}
String field = responseHeaders[pIndex];
// pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK").
if ((pIndex == 0) || (field == null)) {
return field;
}
int idx = field.indexOf(':');
return ((idx > 0)
? field.substring(idx).trim()
: ""); // TODO: "" or null?
}
/**
* Returns the key for the {@code n}<sup>th</sup> header field.
*
* @param pIndex an index.
* @return the key for the {@code n}<sup>th</sup> header field,
* or {@code null} if there are fewer than {@code n}
* fields.
*/
public String getHeaderFieldKey(int pIndex) {
// TODO: getInputStream() first, to make sure we have header fields
if (pIndex >= responseHeaders.length) {
return null;
}
String field = responseHeaders[pIndex];
if (StringUtil.isEmpty(field)) {
return null;
}
int idx = field.indexOf(':');
return StringUtil.toLowerCase(((idx > 0)
? field.substring(0, idx)
: field));
}
/**
* Sets the read timeout for the undelying socket.
* A timeout of zero is interpreted as an
* infinite timeout.
*
* @param pTimeout the maximum time the socket will block for read
* operations, in milliseconds.
*/
public void setTimeout(int pTimeout) {
if (pTimeout < 0) { // Must be positive
throw new IllegalArgumentException("Timeout must be positive.");
}
timeout = pTimeout;
if (socket != null) {
try {
socket.setSoTimeout(pTimeout);
}
catch (SocketException se) {
// Not much to do about that...
}
}
}
/**
* Gets the read timeout for the undelying socket.
*
* @return the maximum time the socket will block for read operations, in
* milliseconds.
* The default value is zero, which is interpreted as an
* infinite timeout.
*/
public int getTimeout() {
try {
return ((socket != null)
? socket.getSoTimeout()
: timeout);
}
catch (SocketException se) {
return timeout;
}
}
/**
* Returns an input stream that reads from this open connection.
*
* @return an input stream that reads from this open connection.
* @throws IOException if an I/O error occurs while
* creating the input stream.
*/
public synchronized InputStream getInputStream() throws IOException {
if (!connected) {
connect();
}
// Nothing to return
if (responseCode == HTTP_NOT_FOUND) {
throw new FileNotFoundException(url.toString());
}
int length;
if (inputStream == null) {
return null;
}
// "De-chunk" the output stream
else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) {
if (!(inputStream instanceof ChunkedInputStream)) {
inputStream = new ChunkedInputStream(inputStream);
}
}
// Make sure we don't wait forever, if the content-length is known
else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) {
if (!(inputStream instanceof FixedLengthInputStream)) {
inputStream = new FixedLengthInputStream(inputStream, length);
}
}
return inputStream;
}
/**
* Returns an output stream that writes to this connection.
*
* @return an output stream that writes to this connection.
* @throws IOException if an I/O error occurs while
* creating the output stream.
*/
public synchronized OutputStream getOutputStream() throws IOException {
if (!connected) {
connect();
}
return outputStream;
}
/**
* Indicates that other requests to the server
* are unlikely in the near future. Calling disconnect()
* should not imply that this HttpURLConnection
* instance can be reused for other requests.
*/
public void disconnect() {
if (socket != null) {
try {
socket.close();
}
catch (IOException ioe) {
// Does not matter, I guess.
}
socket = null;
}
connected = false;
}
/**
* Internal connect method.
*/
private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException {
// Find correct port
final int port = (pURL.getPort() > 0)
? pURL.getPort()
: HTTP_DEFAULT_PORT;
// Create socket if we don't have one
if (socket == null) {
//socket = new Socket(pURL.getHost(), port); // Blocks...
socket = createSocket(pURL, port, connectTimeout);
socket.setSoTimeout(timeout);
}
// Get Socket output stream
OutputStream os = socket.getOutputStream();
// Connect using HTTP
writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType);
// Get response input stream
InputStream sis = socket.getInputStream();
BufferedInputStream is = new BufferedInputStream(sis);
// Detatch reponse headers from reponse input stream
InputStream header = detatchResponseHeader(is);
// Parse headers and set response code/message
responseHeaders = parseResponseHeader(header);
responseHeaderFields = parseHeaderFields(responseHeaders);
//System.err.println("Headers fields:");
//responseHeaderFields.list(System.err);
// Test HTTP response code, to see if further action is needed
switch (getResponseCode()) {
case HTTP_OK:
// 200 OK
inputStream = is;
errorStream = null;
break;
/*
case HTTP_PROXY_AUTH:
// 407 Proxy Authentication Required
*/
case HTTP_UNAUTHORIZED:
// 401 Unauthorized
// Set authorization and try again.. Slightly more compatible
responseCode = -1;
// IS THIS REDIRECTION??
//if (instanceFollowRedirects) { ???
String auth = getHeaderField(HEADER_WWW_AUTH);
// Missing WWW-Authenticate header for 401 response is an error
if (StringUtil.isEmpty(auth)) {
throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage);
}
// Get real mehtod from WWW-Authenticate header
int SP = auth.indexOf(" ");
String method;
String realm = null;
if (SP >= 0) {
method = auth.substring(0, SP);
if (auth.length() >= SP + 7) {
realm = auth.substring(SP + 7); // " realm=".lenght() == 7
}
// else no realm
}
else {
// Default mehtod is Basic
method = SimpleAuthenticator.BASIC;
}
// Get PasswordAuthentication
PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port,
pURL.getProtocol(), realm, method);
// Avoid infinite loop
if (pRetries++ <= 0) {
throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does
}
else if (pa != null) {
connect(pURL, pa, method, pRetries);
}
break;
case HTTP_MOVED_PERM:
// 301 Moved Permanently
case HTTP_MOVED_TEMP:
// 302 Found
case HTTP_SEE_OTHER:
// 303 See Other
/*
case HTTP_USE_PROXY:
// 305 Use Proxy
// How do we handle this?
*/
case HTTP_REDIRECT:
// 307 Temporary Redirect
//System.err.println("Redirecting " + getResponseCode());
if (instanceFollowRedirects) {
// Redirect
responseCode = -1; // Because of the java.net.URLConnection
// getResponseCode implementation...
// ---
// I think redirects must be get?
//setRequestMethod("GET");
// ---
String location = getHeaderField("Location");
URL newLoc = new URL(pURL, location);
// Test if we can reuse the Socket
if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) {
socket.close(); // Close the socket, won't need it anymore
socket = null;
}
if (location != null) {
//System.err.println("Redirecting to " + location);
// Avoid infinite loop
if (--pRetries <= 0) {
throw new ProtocolException("Server redirected too many times (5)");
}
else {
connect(newLoc, pAuth, pAuthType, pRetries);
}
}
break;
}
// ...else, fall through default (if no Location: header)
default :
// Not 200 OK, or any of the redirect responses
// Probably an error...
errorStream = is;
inputStream = null;
}
// --- Need rethinking...
// No further questions, let the Socket wait forever (until the server
// closes the connection)
//socket.setSoTimeout(0);
// Probably not... The timeout should only kick if the read BLOCKS.
// Shutdown output, meaning any writes to the outputstream below will
// probably fail...
//socket.shutdownOutput();
// Not a good idea at all... POSTs need the outputstream to send the
// form-data.
// --- /Need rethinking.
outputStream = os;
}
private static interface SocketConnector extends Runnable {
/**
* Method getSocket
*
* @return the socket
* @throws IOException
*/
public Socket getSocket() throws IOException;
}
/**
* Creates a socket to the given URL and port, with the given connect
* timeout. If the socket waits more than the given timout to connect,
* an ConnectException is thrown.
*
* @param pURL the URL to connect to
* @param pPort the port to connect to
* @param pConnectTimeout the connect timeout
* @return the created Socket.
* @throws ConnectException if the connection is refused or otherwise
* times out.
* @throws UnknownHostException if the IP address of the host could not be
* determined.
* @throws IOException if an I/O error occurs when creating the socket.
* @todo Move this code to a SocetImpl or similar?
* @see Socket#Socket(String,int)
*/
private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException {
Socket socket;
final Object current = this;
SocketConnector connector;
Thread t = new Thread(connector = new SocketConnector() {
private IOException mConnectException = null;
private Socket mLocalSocket = null;
public Socket getSocket() throws IOException {
if (mConnectException != null) {
throw mConnectException;
}
return mLocalSocket;
}
// Run method
public void run() {
try {
mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks...
}
catch (IOException ioe) {
// Store this exception for later
mConnectException = ioe;
}
// Signal that we are done
synchronized (current) {
current.notify();
}
}
});
t.start();
// Wait for connect
synchronized (this) {
try {
/// Only wait if thread is alive!
if (t.isAlive()) {
if (pConnectTimeout > 0) {
wait(pConnectTimeout);
}
else {
wait();
}
}
}
catch (InterruptedException ie) {
// Continue excecution on interrupt? Hmmm..
}
}
// Throw exception if the socket didn't connect fast enough
if ((socket = connector.getSocket()) == null) {
throw new ConnectException("Socket connect timed out!");
}
return socket;
}
/**
* Opens a communications link to the resource referenced by this
* URL, if such a connection has not already been established.
* <p/>
* If the {@code connect} method is called when the connection
* has already been opened (indicated by the {@code connected}
* field having the value {@code true}), the call is ignored.
* <p/>
* URLConnection objects go through two phases: first they are
* created, then they are connected. After being created, and
* before being connected, various options can be specified
* (e.g., doInput and UseCaches). After connecting, it is an
* error to try to set them. Operations that depend on being
* connected, like getContentLength, will implicitly perform the
* connection, if necessary.
*
* @throws IOException if an I/O error occurs while opening the
* connection.
* @see java.net.URLConnection#connected
* @see <A href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">RFC 2616</A>
*/
public void connect() throws IOException {
if (connected) {
return; // Ignore
}
connected = true;
connect(url, null, null, maxRedirects);
}
/**
* TODO: Proxy support is still missing.
*
* @return this method returns false, as proxy suport is not implemented.
*/
public boolean usingProxy() {
return false;
}
/**
* Writes the HTTP request headers, for HTTP GET method.
*
* @see <A href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">RFC 2616</A>
*/
private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy,
PasswordAuthentication pAuth, String pAuthType) {
PrintWriter out = new PrintWriter(pOut, true); // autoFlush
if (!pUsingProxy) {
out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath())
? pURL.getPath()
: "/") + ((pURL.getQuery() != null)
? "?" + pURL.getQuery()
: "") + " HTTP/1.1"); // HTTP/1.1
// out.println("Connection: close"); // No persistent connections yet
/*
System.err.println(pMethod + " "
+ (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/")
+ (pURL.getQuery() != null ? "?" + pURL.getQuery() : "")
+ " HTTP/1.1"); // HTTP/1.1
*/
// Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0)
out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1)
? ":" + pURL.getPort()
: ""));
/*
System.err.println("Host: " + pURL.getHost()
+ (pURL.getPort() != -1 ? ":" + pURL.getPort() : ""));
*/
}
else {
////-- PROXY (absolute) VERSION
out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1)
? ":" + pURL.getPort()
: "") + pURL.getPath() + ((pURL.getQuery() != null)
? "?" + pURL.getQuery()
: "") + " HTTP/1.1");
}
// Check if we have authentication
if (pAuth != null) {
// If found, set Authorization header
byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes();
// "Authorization" ":" credentials
out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass));
/*
System.err.println("Authorization: " + pAuthType + " "
+ BASE64.encode(userPass));
*/
}
// Iterate over properties
for (Map.Entry<Object, Object> property : pProps.entrySet()) {
out.println(property.getKey() + ": " + property.getValue());
//System.err.println(property.getKey() + ": " + property.getValue());
}
out.println(); // Empty line, marks end of request-header
}
/**
* Finds the end of the HTTP response header in an array of bytes.
*
* @todo This one's a little dirty...
*/
private static int findEndOfHeader(byte[] pBytes, int pEnd) {
byte[] header = HTTP_HEADER_END.getBytes();
// Normal condition, check all bytes
for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match
if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) {
//System.err.println("FOUND END OF HEADER!");
return i + 4;
}
}
// Check last 3 bytes, to check if we have a partial match
if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) {
//System.err.println("FOUND LAST BYTE");
return -2; // LAST BYTE
}
else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) {
//System.err.println("FOUND LAST TWO BYTES");
return -3; // LAST TWO BYTES
}
else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) {
//System.err.println("FOUND LAST THREE BYTES");
return -4; // LAST THREE BYTES
}
return -1; // NO BYTES MATCH
}
/**
* Reads the header part of the response, and copies it to a different
* InputStream.
*/
private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException {
// Store header in byte array
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
pIS.mark(BUF_SIZE);
byte[] buffer = new byte[BUF_SIZE];
int length;
int headerEnd;
// Read from iput, store in bytes
while ((length = pIS.read(buffer)) != -1) {
// End of header?
headerEnd = findEndOfHeader(buffer, length);
if (headerEnd >= 0) {
// Write rest
bytes.write(buffer, 0, headerEnd);
// Go back to last mark
pIS.reset();
// Position stream to right after header, and exit loop
pIS.skip(headerEnd);
break;
}
else if (headerEnd < -1) {
// Write partial (except matching header bytes)
bytes.write(buffer, 0, length - 4);
// Go back to last mark
pIS.reset();
// Position stream to right before potential header end
pIS.skip(length - 4);
}
else {
// Write all
bytes.write(buffer, 0, length);
}
// Can't read more than BUF_SIZE ahead anyway
pIS.mark(BUF_SIZE);
}
return new ByteArrayInputStream(bytes.toByteArray());
}
/**
* Pareses the response header fields.
*/
private static Properties parseHeaderFields(String[] pHeaders) {
Properties headers = new Properties();
// Get header information
int split;
String field;
String value;
for (String header : pHeaders) {
//System.err.println(pHeaders[i]);
if ((split = header.indexOf(":")) > 0) {
// Read & parse..?
field = header.substring(0, split);
value = header.substring(split + 1);
//System.err.println(field + ": " + value.trim());
headers.setProperty(StringUtil.toLowerCase(field), value.trim());
}
}
return headers;
}
/**
* Parses the response headers.
*/
private static String[] parseResponseHeader(InputStream pIS) throws IOException {
List<String> headers = new ArrayList<String>();
// Wrap Stream in Reader
BufferedReader in = new BufferedReader(new InputStreamReader(pIS));
// Get response status
String header;
while ((header = in.readLine()) != null) {
//System.err.println(header);
headers.add(header);
}
return headers.toArray(new String[headers.size()]);
}
/**
* A FilterInputStream that wraps HTTP streams, with given content-length.
*/
protected static class FixedLengthInputStream extends FilterInputStream {
private int mBytesLeft = 0;
protected FixedLengthInputStream(InputStream pIS, int pLength) {
super(pIS);
mBytesLeft = pLength;
}
public int available() throws IOException {
int available = in.available();
return ((available < mBytesLeft)
? available
: mBytesLeft);
}
public int read() throws IOException {
if (mBytesLeft-- > 0) {
return in.read();
}
return -1;
}
public int read(byte[] pBytes, int pOffset, int pLength) throws IOException {
int read;
if (mBytesLeft <= 0) {
return -1; // EOF
}
else if (mBytesLeft < pLength) {
// Read all available
read = in.read(pBytes, pOffset, mBytesLeft);
//System.err.println("Reading partial: " + read);
mBytesLeft -= read;
return read;
}
// Just read
read = in.read(pBytes, pOffset, pLength);
//System.err.println("Reading all avail: " + read);
mBytesLeft -= read;
return read;
}
}
/**
* A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode.
*/
protected static class ChunkedInputStream extends FilterInputStream {
private int mAvailableInCurrentChunk = 0;
/**
* Creates an input streams that removes the "chunk-headers" and
* makes it look like any other input stream.
*/
protected ChunkedInputStream(InputStream pIS) {
super(pIS);
if (pIS == null) {
throw new IllegalArgumentException("InputStream may not be null!");
}
}
/**
* Returns the number of bytes that can be read from this input stream
* without blocking.
* <P>
* This version returns whatever is less of in.available() and the
* length of the current chunk.
*
* @return the number of bytes that can be read from the input stream
* without blocking.
* @throws IOException if an I/O error occurs.
* @see #in
*/
public int available() throws IOException {
if (mAvailableInCurrentChunk == 0) {
mAvailableInCurrentChunk = parseChunkSize();
}
int realAvail = in.available();
return (mAvailableInCurrentChunk < realAvail)
? mAvailableInCurrentChunk
: realAvail;
}
/**
* Reads up to len bytes of data from this input stream into an array
* of bytes. This method blocks until some input is available.
* <P>
* This version will read up to len bytes of data, or as much as is
* available in the current chunk. If there is no more data in the
* curernt chunk, the method will read the size of the next chunk, and
* read from that, until the last chunk is read (a chunk with a size of
* 0).
*
* @param pBytes the buffer into which the data is read.
* @param pOffset the start offset of the data.
* @param pLength the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or -1 if
* there is no more data because the end of the stream has been
* reached.
* @throws IOException if an I/O error occurs.
* @see #in
*/
public int read(byte[] pBytes, int pOffset, int pLength) throws IOException {
//System.err.println("Avail: " + mAvailableInCurrentChunk
// + " length: " + pLength);
int read;
if (mAvailableInCurrentChunk == -1) {
return -1; // EOF
}
if (mAvailableInCurrentChunk == 0) {
//System.err.println("Nothing to read, parsing size!");
// If nothing is read so far, read chunk header
mAvailableInCurrentChunk = parseChunkSize();
return read(pBytes, pOffset, pLength);
}
else if (mAvailableInCurrentChunk < pLength) {
// Read all available
read = in.read(pBytes, pOffset, mAvailableInCurrentChunk);
//System.err.println("Reading partial: " + read);
mAvailableInCurrentChunk -= read;
return read;
}
// Just read
read = in.read(pBytes, pOffset, pLength);
//System.err.println("Reading all avail: " + read);
mAvailableInCurrentChunk -= read;
return read;
}
/**
* Reads the next byte of data from this input stream. The value byte
* is returned as an int in the range 0 to 255. If no byte is available
* because the end of the stream has been reached, the value -1 is
* returned. This method blocks until input data is available, the end
* of the stream is detected, or an exception is thrown.
* <P>
* This version reads one byte of data from the current chunk as long
* as there is more data in the chunk. If there is no more data in the
* curernt chunk, the method will read the size of the next chunk, and
* read from that, until the last chunk is read (a chunk with a size of
* 0).
*
* @return the next byte of data, or -1 if the end of the stream is
* reached.
* @see #in
*/
public int read() throws IOException {
// We have no data, parse chunk header
if (mAvailableInCurrentChunk == -1) {
return -1;
}
else if (mAvailableInCurrentChunk == 0) {
// Next chunk!
mAvailableInCurrentChunk = parseChunkSize();
return read();
}
mAvailableInCurrentChunk--;
return in.read();
}
/**
* Reads the chunk size from the chunk header
* {@code chunk-size [SP chunk-extension] CRLF}.
* The chunk-extension is simply discarded.
*
* @return the length of the current chunk, or -1 if the current chunk
* is the last-chunk (a chunk with the size of 0).
*/
protected int parseChunkSize() throws IOException {
StringBuilder buf = new StringBuilder();
int b;
// read chunk-size, chunk-extension (if any) and CRLF
while ((b = in.read()) > 0) {
if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF
break; // except for this one...
}
buf.append((char) b);
}
String line = buf.toString();
// Happens, as we don't read CRLF off the end of the chunk data...
if (line.length() == 0) {
return 0;
}
// Discard any chunk-extensions, and read size (HEX).
int spIdx = line.indexOf(' ');
int size = Integer.parseInt(((spIdx >= 0)
? line.substring(0, spIdx)
: line), 16);
// This is the last chunk (=EOF)
if (size == 0) {
return -1;
}
return size;
}
}
}