michael@0: /*
michael@0: * ====================================================================
michael@0: * Licensed to the Apache Software Foundation (ASF) under one
michael@0: * or more contributor license agreements. See the NOTICE file
michael@0: * distributed with this work for additional information
michael@0: * regarding copyright ownership. The ASF licenses this file
michael@0: * to you under the Apache License, Version 2.0 (the
michael@0: * "License"); you may not use this file except in compliance
michael@0: * with the License. You may obtain a copy of the License at
michael@0: *
michael@0: * http://www.apache.org/licenses/LICENSE-2.0
michael@0: *
michael@0: * Unless required by applicable law or agreed to in writing,
michael@0: * software distributed under the License is distributed on an
michael@0: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
michael@0: * KIND, either express or implied. See the License for the
michael@0: * specific language governing permissions and limitations
michael@0: * under the License.
michael@0: * ====================================================================
michael@0: *
michael@0: * This software consists of voluntary contributions made by many
michael@0: * individuals on behalf of the Apache Software Foundation. For more
michael@0: * information on the Apache Software Foundation, please see
michael@0: *
michael@0: * Note that this class NEVER closes the underlying stream, even when close michael@0: * gets called. Instead, it will read until the "end" of its chunking on michael@0: * close, which allows for the seamless execution of subsequent HTTP 1.1 michael@0: * requests, while not requiring the client to remember to read the entire michael@0: * contents of the response. michael@0: * michael@0: * michael@0: * @since 4.0 michael@0: * michael@0: */ michael@0: public class ChunkedInputStream extends InputStream { michael@0: michael@0: private static final int CHUNK_LEN = 1; michael@0: private static final int CHUNK_DATA = 2; michael@0: private static final int CHUNK_CRLF = 3; michael@0: michael@0: private static final int BUFFER_SIZE = 2048; michael@0: michael@0: /** The session input buffer */ michael@0: private final SessionInputBuffer in; michael@0: michael@0: private final CharArrayBuffer buffer; michael@0: michael@0: private int state; michael@0: michael@0: /** The chunk size */ michael@0: private int chunkSize; michael@0: michael@0: /** The current position within the current chunk */ michael@0: private int pos; michael@0: michael@0: /** True if we've reached the end of stream */ michael@0: private boolean eof = false; michael@0: michael@0: /** True if this stream is closed */ michael@0: private boolean closed = false; michael@0: michael@0: private Header[] footers = new Header[] {}; michael@0: michael@0: /** michael@0: * Wraps session input stream and reads chunk coded input. michael@0: * michael@0: * @param in The session input buffer michael@0: */ michael@0: public ChunkedInputStream(final SessionInputBuffer in) { michael@0: super(); michael@0: if (in == null) { michael@0: throw new IllegalArgumentException("Session input buffer may not be null"); michael@0: } michael@0: this.in = in; michael@0: this.pos = 0; michael@0: this.buffer = new CharArrayBuffer(16); michael@0: this.state = CHUNK_LEN; michael@0: } michael@0: michael@0: public int available() throws IOException { michael@0: if (this.in instanceof BufferInfo) { michael@0: int len = ((BufferInfo) this.in).length(); michael@0: return Math.min(len, this.chunkSize - this.pos); michael@0: } else { michael@0: return 0; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: *
Returns all the data in a chunked stream in coalesced form. A chunk michael@0: * is followed by a CRLF. The method returns -1 as soon as a chunksize of 0 michael@0: * is detected.
michael@0: * michael@0: *Trailer headers are read automatically at the end of the stream and michael@0: * can be obtained with the getResponseFooters() method.
michael@0: * michael@0: * @return -1 of the end of the stream has been reached or the next data michael@0: * byte michael@0: * @throws IOException in case of an I/O error michael@0: */ michael@0: public int read() throws IOException { michael@0: if (this.closed) { michael@0: throw new IOException("Attempted read from closed stream."); michael@0: } michael@0: if (this.eof) { michael@0: return -1; michael@0: } michael@0: if (state != CHUNK_DATA) { michael@0: nextChunk(); michael@0: if (this.eof) { michael@0: return -1; michael@0: } michael@0: } michael@0: int b = in.read(); michael@0: if (b != -1) { michael@0: pos++; michael@0: if (pos >= chunkSize) { michael@0: state = CHUNK_CRLF; michael@0: } michael@0: } michael@0: return b; michael@0: } michael@0: michael@0: /** michael@0: * Read some bytes from the stream. michael@0: * @param b The byte array that will hold the contents from the stream. michael@0: * @param off The offset into the byte array at which bytes will start to be michael@0: * placed. michael@0: * @param len the maximum number of bytes that can be returned. michael@0: * @return The number of bytes returned or -1 if the end of stream has been michael@0: * reached. michael@0: * @throws IOException in case of an I/O error michael@0: */ michael@0: public int read (byte[] b, int off, int len) throws IOException { michael@0: michael@0: if (closed) { michael@0: throw new IOException("Attempted read from closed stream."); michael@0: } michael@0: michael@0: if (eof) { michael@0: return -1; michael@0: } michael@0: if (state != CHUNK_DATA) { michael@0: nextChunk(); michael@0: if (eof) { michael@0: return -1; michael@0: } michael@0: } michael@0: len = Math.min(len, chunkSize - pos); michael@0: int bytesRead = in.read(b, off, len); michael@0: if (bytesRead != -1) { michael@0: pos += bytesRead; michael@0: if (pos >= chunkSize) { michael@0: state = CHUNK_CRLF; michael@0: } michael@0: return bytesRead; michael@0: } else { michael@0: eof = true; michael@0: throw new TruncatedChunkException("Truncated chunk " michael@0: + "( expected size: " + chunkSize michael@0: + "; actual size: " + pos + ")"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Read some bytes from the stream. michael@0: * @param b The byte array that will hold the contents from the stream. michael@0: * @return The number of bytes returned or -1 if the end of stream has been michael@0: * reached. michael@0: * @throws IOException in case of an I/O error michael@0: */ michael@0: public int read (byte[] b) throws IOException { michael@0: return read(b, 0, b.length); michael@0: } michael@0: michael@0: /** michael@0: * Read the next chunk. michael@0: * @throws IOException in case of an I/O error michael@0: */ michael@0: private void nextChunk() throws IOException { michael@0: chunkSize = getChunkSize(); michael@0: if (chunkSize < 0) { michael@0: throw new MalformedChunkCodingException("Negative chunk size"); michael@0: } michael@0: state = CHUNK_DATA; michael@0: pos = 0; michael@0: if (chunkSize == 0) { michael@0: eof = true; michael@0: parseTrailerHeaders(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Expects the stream to start with a chunksize in hex with optional michael@0: * comments after a semicolon. The line must end with a CRLF: "a3; some michael@0: * comment\r\n" Positions the stream at the start of the next line. michael@0: * michael@0: * @param in The new input stream. michael@0: * @param required true if a valid chunk must be present, michael@0: * false otherwise. michael@0: * michael@0: * @return the chunk size as integer michael@0: * michael@0: * @throws IOException when the chunk size could not be parsed michael@0: */ michael@0: private int getChunkSize() throws IOException { michael@0: int st = this.state; michael@0: switch (st) { michael@0: case CHUNK_CRLF: michael@0: this.buffer.clear(); michael@0: int i = this.in.readLine(this.buffer); michael@0: if (i == -1) { michael@0: return 0; michael@0: } michael@0: if (!this.buffer.isEmpty()) { michael@0: throw new MalformedChunkCodingException( michael@0: "Unexpected content at the end of chunk"); michael@0: } michael@0: state = CHUNK_LEN; michael@0: //$FALL-THROUGH$ michael@0: case CHUNK_LEN: michael@0: this.buffer.clear(); michael@0: i = this.in.readLine(this.buffer); michael@0: if (i == -1) { michael@0: return 0; michael@0: } michael@0: int separator = this.buffer.indexOf(';'); michael@0: if (separator < 0) { michael@0: separator = this.buffer.length(); michael@0: } michael@0: try { michael@0: return Integer.parseInt(this.buffer.substringTrimmed(0, separator), 16); michael@0: } catch (NumberFormatException e) { michael@0: throw new MalformedChunkCodingException("Bad chunk header"); michael@0: } michael@0: default: michael@0: throw new IllegalStateException("Inconsistent codec state"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Reads and stores the Trailer headers. michael@0: * @throws IOException in case of an I/O error michael@0: */ michael@0: private void parseTrailerHeaders() throws IOException { michael@0: try { michael@0: this.footers = AbstractMessageParser.parseHeaders michael@0: (in, -1, -1, null); michael@0: } catch (HttpException e) { michael@0: IOException ioe = new MalformedChunkCodingException("Invalid footer: " michael@0: + e.getMessage()); michael@0: ExceptionUtils.initCause(ioe, e); michael@0: throw ioe; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Upon close, this reads the remainder of the chunked message, michael@0: * leaving the underlying socket at a position to start reading the michael@0: * next response without scanning. michael@0: * @throws IOException in case of an I/O error michael@0: */ michael@0: public void close() throws IOException { michael@0: if (!closed) { michael@0: try { michael@0: if (!eof) { michael@0: // read and discard the remainder of the message michael@0: byte buffer[] = new byte[BUFFER_SIZE]; michael@0: while (read(buffer) >= 0) { michael@0: } michael@0: } michael@0: } finally { michael@0: eof = true; michael@0: closed = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: public Header[] getFooters() { michael@0: return (Header[])this.footers.clone(); michael@0: } michael@0: michael@0: }