ChunkedInputStream.java revision 417f3b92ba4549b2f22340e3107d869d2b9c5bb8
1/*
2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/impl/io/ChunkedInputStream.java $
3 * $Revision: 569843 $
4 * $Date: 2007-08-26 10:05:40 -0700 (Sun, 26 Aug 2007) $
5 *
6 * ====================================================================
7 * Licensed to the Apache Software Foundation (ASF) under one
8 * or more contributor license agreements.  See the NOTICE file
9 * distributed with this work for additional information
10 * regarding copyright ownership.  The ASF licenses this file
11 * to you under the Apache License, Version 2.0 (the
12 * "License"); you may not use this file except in compliance
13 * with the License.  You may obtain a copy of the License at
14 *
15 *   http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing,
18 * software distributed under the License is distributed on an
19 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20 * KIND, either express or implied.  See the License for the
21 * specific language governing permissions and limitations
22 * under the License.
23 * ====================================================================
24 *
25 * This software consists of voluntary contributions made by many
26 * individuals on behalf of the Apache Software Foundation.  For more
27 * information on the Apache Software Foundation, please see
28 * <http://www.apache.org/>.
29 *
30 */
31
32package org.apache.http.impl.io;
33
34import java.io.IOException;
35import java.io.InputStream;
36
37import org.apache.http.Header;
38import org.apache.http.HttpException;
39import org.apache.http.MalformedChunkCodingException;
40import org.apache.http.io.SessionInputBuffer;
41import org.apache.http.protocol.HTTP;
42import org.apache.http.util.CharArrayBuffer;
43import org.apache.http.util.ExceptionUtils;
44
45/**
46 * Implements chunked transfer coding.
47 * See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616.txt">RFC 2616</a>,
48 * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6">section 3.6.1</a>.
49 * It transparently coalesces chunks of a HTTP stream that uses chunked
50 * transfer coding. After the stream is read to the end, it provides access
51 * to the trailers, if any.
52 * <p>
53 * Note that this class NEVER closes the underlying stream, even when close
54 * gets called.  Instead, it will read until the "end" of its chunking on
55 * close, which allows for the seamless execution of subsequent HTTP 1.1
56 * requests, while not requiring the client to remember to read the entire
57 * contents of the response.
58 * </p>
59 *
60 * @author Ortwin Glueck
61 * @author Sean C. Sullivan
62 * @author Martin Elwin
63 * @author Eric Johnson
64 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
65 * @author Michael Becke
66 * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
67 *
68 * @since 4.0
69 *
70 */
71public class ChunkedInputStream extends InputStream {
72
73    /** The session input buffer */
74    private SessionInputBuffer in;
75
76    private final CharArrayBuffer buffer;
77
78    /** The chunk size */
79    private int chunkSize;
80
81    /** The current position within the current chunk */
82    private int pos;
83
84    /** True if we'are at the beginning of stream */
85    private boolean bof = true;
86
87    /** True if we've reached the end of stream */
88    private boolean eof = false;
89
90    /** True if this stream is closed */
91    private boolean closed = false;
92
93    private Header[] footers = new Header[] {};
94
95    public ChunkedInputStream(final SessionInputBuffer in) {
96        super();
97        if (in == null) {
98            throw new IllegalArgumentException("Session input buffer may not be null");
99        }
100        this.in = in;
101        this.pos = 0;
102        this.buffer = new CharArrayBuffer(16);
103    }
104
105    /**
106     * <p> Returns all the data in a chunked stream in coalesced form. A chunk
107     * is followed by a CRLF. The method returns -1 as soon as a chunksize of 0
108     * is detected.</p>
109     *
110     * <p> Trailer headers are read automcatically at the end of the stream and
111     * can be obtained with the getResponseFooters() method.</p>
112     *
113     * @return -1 of the end of the stream has been reached or the next data
114     * byte
115     * @throws IOException If an IO problem occurs
116     */
117    public int read() throws IOException {
118        if (this.closed) {
119            throw new IOException("Attempted read from closed stream.");
120        }
121        if (this.eof) {
122            return -1;
123        }
124        if (this.pos >= this.chunkSize) {
125            nextChunk();
126            if (this.eof) {
127                return -1;
128            }
129        }
130        pos++;
131        return in.read();
132    }
133
134    /**
135     * Read some bytes from the stream.
136     * @param b The byte array that will hold the contents from the stream.
137     * @param off The offset into the byte array at which bytes will start to be
138     * placed.
139     * @param len the maximum number of bytes that can be returned.
140     * @return The number of bytes returned or -1 if the end of stream has been
141     * reached.
142     * @see java.io.InputStream#read(byte[], int, int)
143     * @throws IOException if an IO problem occurs.
144     */
145    public int read (byte[] b, int off, int len) throws IOException {
146
147        if (closed) {
148            throw new IOException("Attempted read from closed stream.");
149        }
150
151        if (eof) {
152            return -1;
153        }
154        if (pos >= chunkSize) {
155            nextChunk();
156            if (eof) {
157                return -1;
158            }
159        }
160        len = Math.min(len, chunkSize - pos);
161        int count = in.read(b, off, len);
162        pos += count;
163        return count;
164    }
165
166    /**
167     * Read some bytes from the stream.
168     * @param b The byte array that will hold the contents from the stream.
169     * @return The number of bytes returned or -1 if the end of stream has been
170     * reached.
171     * @see java.io.InputStream#read(byte[])
172     * @throws IOException if an IO problem occurs.
173     */
174    public int read (byte[] b) throws IOException {
175        return read(b, 0, b.length);
176    }
177
178    /**
179     * Read the next chunk.
180     * @throws IOException If an IO error occurs.
181     */
182    private void nextChunk() throws IOException {
183        chunkSize = getChunkSize();
184        if (chunkSize < 0) {
185            throw new MalformedChunkCodingException("Negative chunk size");
186        }
187        bof = false;
188        pos = 0;
189        if (chunkSize == 0) {
190            eof = true;
191            parseTrailerHeaders();
192        }
193    }
194
195    /**
196     * Expects the stream to start with a chunksize in hex with optional
197     * comments after a semicolon. The line must end with a CRLF: "a3; some
198     * comment\r\n" Positions the stream at the start of the next line.
199     *
200     * @param in The new input stream.
201     * @param required <tt>true<tt/> if a valid chunk must be present,
202     *                 <tt>false<tt/> otherwise.
203     *
204     * @return the chunk size as integer
205     *
206     * @throws IOException when the chunk size could not be parsed
207     */
208    private int getChunkSize() throws IOException {
209        // skip CRLF
210        if (!bof) {
211            int cr = in.read();
212            int lf = in.read();
213            if ((cr != HTTP.CR) || (lf != HTTP.LF)) {
214                throw new MalformedChunkCodingException(
215                    "CRLF expected at end of chunk");
216            }
217        }
218        //parse data
219        this.buffer.clear();
220        int i = this.in.readLine(this.buffer);
221        if (i == -1) {
222            throw new MalformedChunkCodingException(
223                    "Chunked stream ended unexpectedly");
224        }
225        int separator = this.buffer.indexOf(';');
226        if (separator < 0) {
227            separator = this.buffer.length();
228        }
229        try {
230            return Integer.parseInt(this.buffer.substringTrimmed(0, separator), 16);
231        } catch (NumberFormatException e) {
232            throw new MalformedChunkCodingException("Bad chunk header");
233        }
234    }
235
236    /**
237     * Reads and stores the Trailer headers.
238     * @throws IOException If an IO problem occurs
239     */
240    private void parseTrailerHeaders() throws IOException {
241        try {
242            this.footers = AbstractMessageParser.parseHeaders
243                (in, -1, -1, null);
244        } catch (HttpException e) {
245            IOException ioe = new MalformedChunkCodingException("Invalid footer: "
246                    + e.getMessage());
247            ExceptionUtils.initCause(ioe, e);
248            throw ioe;
249        }
250    }
251
252    /**
253     * Upon close, this reads the remainder of the chunked message,
254     * leaving the underlying socket at a position to start reading the
255     * next response without scanning.
256     * @throws IOException If an IO problem occurs.
257     */
258    public void close() throws IOException {
259        if (!closed) {
260            try {
261                if (!eof) {
262                    exhaustInputStream(this);
263                }
264            } finally {
265                eof = true;
266                closed = true;
267            }
268        }
269    }
270
271    public Header[] getFooters() {
272        return (Header[])this.footers.clone();
273    }
274
275    /**
276     * Exhaust an input stream, reading until EOF has been encountered.
277     *
278     * <p>Note that this function is intended as a non-public utility.
279     * This is a little weird, but it seemed silly to make a utility
280     * class for this one function, so instead it is just static and
281     * shared that way.</p>
282     *
283     * @param inStream The {@link InputStream} to exhaust.
284     * @throws IOException If an IO problem occurs
285     */
286    static void exhaustInputStream(final InputStream inStream) throws IOException {
287        // read and discard the remainder of the message
288        byte buffer[] = new byte[1024];
289        while (inStream.read(buffer) >= 0) {
290            ;
291        }
292    }
293
294}
295