+ // HTTPInputStream ///////////////////////////////////////////////////////////////////////////////////
+
+ /** An input stream that represents a subset of a longer input stream. Supports HTTP chunking as well */
+ public class HTTPInputStream extends FilterInputStream {
+
+ /** if chunking, the number of bytes remaining in this subset; otherwise the remainder of the chunk */
+ private int length = 0;
+
+ /** this semaphore will be released when the stream is closed */
+ private Semaphore releaseMe = null;
+
+ /** indicates that we have encountered the zero-length terminator chunk */
+ boolean chunkedDone = false;
+
+ /** if we're on the first chunk, we don't pre-read a CRLF */
+ boolean firstChunk = true;
+
+ /** the length of the entire content body; -1 if chunked */
+ private int contentLength = 0;
+ public int getContentLength() { return contentLength; }
+
+ HTTPInputStream(InputStream in, int length, Semaphore releaseMe) throws IOException {
+ super(in);
+ this.releaseMe = releaseMe;
+ this.contentLength = length;
+ this.length = length == -1 ? 0 : length;
+ }
+
+ public boolean markSupported() { return false; }
+ public int read(byte[] b) throws IOException { return read(b, 0, b.length); }
+ public long skip(long n) throws IOException { return read(null, -1, (int)n); }
+ public int available() throws IOException {
+ if (contentLength == -1) return java.lang.Math.min(super.available(), length);
+ return super.available();
+ }
+
+ public int read() throws IOException {
+ byte[] b = new byte[1];
+ int ret = read(b, 0, 1);
+ return ret == -1 ? -1 : b[0] & 0xff;
+ }
+
+ private void readChunk() throws IOException {
+ if (chunkedDone) return;
+ if (!firstChunk) super.skip(2); // CRLF
+ firstChunk = false;
+ String chunkLen = "";
+ while(true) {
+ int i = super.read();
+ if (i == -1) throw new HTTPException("encountered end of stream while reading chunk length");
+
+ // FEATURE: handle chunking extensions
+ if (i == '\r') {
+ super.read(); // LF
+ break;
+ } else {
+ chunkLen += (char)i;
+ }
+ }
+ length = Integer.parseInt(chunkLen.trim(), 16);
+ if (length == 0) chunkedDone = true;
+ }
+
+ public int read(byte[] b, int off, int len) throws IOException {
+ boolean good = false;
+ try {
+ if (length == 0 && contentLength == -1) {
+ readChunk();
+ if (chunkedDone) { good = true; return -1; }
+ } else {
+ if (length == 0) { good = true; return -1; }
+ }
+ if (len > length) len = length;
+ int ret = b == null ? (int)super.skip(len) : super.read(b, off, len);
+ if (ret >= 0) {
+ length -= ret;
+ good = true;
+ }
+ return ret;
+ } finally {
+ if (!good) invalid = true;
+ }
+ }
+
+ public void close() throws IOException {
+ if (contentLength == -1) {
+ while(!chunkedDone) {
+ if (length != 0) skip(length);
+ readChunk();
+ }
+ skip(2);
+ } else {
+ if (length != 0) skip(length);
+ }
+ if (releaseMe != null) releaseMe.release();
+ }
+ }
+
+
+ // Misc Helpers ///////////////////////////////////////////////////////////////////////////////////
+
+ /** reads a set of HTTP headers off of the input stream, returning null if the stream is already at its end */
+ private Hashtable parseHeaders(InputStream in) throws IOException {
+ Hashtable ret = new Hashtable();
+
+ // we can't use a BufferedReader directly on the input stream, since it will buffer past the end of the headers