001: /*
002: * $HeadURL: https://svn.apache.org/repos/asf/httpcomponents/httpcore/tags/4.0-beta1/module-main/src/main/java/org/apache/http/impl/io/ChunkedInputStream.java $
003: * $Revision: 569843 $
004: * $Date: 2007-08-26 19:05:40 +0200 (Sun, 26 Aug 2007) $
005: *
006: * ====================================================================
007: * Licensed to the Apache Software Foundation (ASF) under one
008: * or more contributor license agreements. See the NOTICE file
009: * distributed with this work for additional information
010: * regarding copyright ownership. The ASF licenses this file
011: * to you under the Apache License, Version 2.0 (the
012: * "License"); you may not use this file except in compliance
013: * with the License. You may obtain a copy of the License at
014: *
015: * http://www.apache.org/licenses/LICENSE-2.0
016: *
017: * Unless required by applicable law or agreed to in writing,
018: * software distributed under the License is distributed on an
019: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
020: * KIND, either express or implied. See the License for the
021: * specific language governing permissions and limitations
022: * under the License.
023: * ====================================================================
024: *
025: * This software consists of voluntary contributions made by many
026: * individuals on behalf of the Apache Software Foundation. For more
027: * information on the Apache Software Foundation, please see
028: * <http://www.apache.org/>.
029: *
030: */
031:
032: package org.apache.http.impl.io;
033:
034: import java.io.IOException;
035: import java.io.InputStream;
036:
037: import org.apache.http.Header;
038: import org.apache.http.HttpException;
039: import org.apache.http.MalformedChunkCodingException;
040: import org.apache.http.io.SessionInputBuffer;
041: import org.apache.http.protocol.HTTP;
042: import org.apache.http.util.CharArrayBuffer;
043: import org.apache.http.util.ExceptionUtils;
044:
045: /**
046: * Implements chunked transfer coding.
047: * See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616.txt">RFC 2616</a>,
048: * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6">section 3.6.1</a>.
049: * It transparently coalesces chunks of a HTTP stream that uses chunked
050: * transfer coding. After the stream is read to the end, it provides access
051: * to the trailers, if any.
052: * <p>
053: * Note that this class NEVER closes the underlying stream, even when close
054: * gets called. Instead, it will read until the "end" of its chunking on
055: * close, which allows for the seamless execution of subsequent HTTP 1.1
056: * requests, while not requiring the client to remember to read the entire
057: * contents of the response.
058: * </p>
059: *
060: * @author Ortwin Glueck
061: * @author Sean C. Sullivan
062: * @author Martin Elwin
063: * @author Eric Johnson
064: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
065: * @author Michael Becke
066: * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
067: *
068: * @since 4.0
069: *
070: */
071: public class ChunkedInputStream extends InputStream {
072:
073: /** The session input buffer */
074: private SessionInputBuffer in;
075:
076: private final CharArrayBuffer buffer;
077:
078: /** The chunk size */
079: private int chunkSize;
080:
081: /** The current position within the current chunk */
082: private int pos;
083:
084: /** True if we'are at the beginning of stream */
085: private boolean bof = true;
086:
087: /** True if we've reached the end of stream */
088: private boolean eof = false;
089:
090: /** True if this stream is closed */
091: private boolean closed = false;
092:
093: private Header[] footers = new Header[] {};
094:
095: public ChunkedInputStream(final SessionInputBuffer in) {
096: super ();
097: if (in == null) {
098: throw new IllegalArgumentException(
099: "Session input buffer may not be null");
100: }
101: this .in = in;
102: this .pos = 0;
103: this .buffer = new CharArrayBuffer(16);
104: }
105:
106: /**
107: * <p> Returns all the data in a chunked stream in coalesced form. A chunk
108: * is followed by a CRLF. The method returns -1 as soon as a chunksize of 0
109: * is detected.</p>
110: *
111: * <p> Trailer headers are read automcatically at the end of the stream and
112: * can be obtained with the getResponseFooters() method.</p>
113: *
114: * @return -1 of the end of the stream has been reached or the next data
115: * byte
116: * @throws IOException If an IO problem occurs
117: */
118: public int read() throws IOException {
119: if (this .closed) {
120: throw new IOException("Attempted read from closed stream.");
121: }
122: if (this .eof) {
123: return -1;
124: }
125: if (this .pos >= this .chunkSize) {
126: nextChunk();
127: if (this .eof) {
128: return -1;
129: }
130: }
131: pos++;
132: return in.read();
133: }
134:
135: /**
136: * Read some bytes from the stream.
137: * @param b The byte array that will hold the contents from the stream.
138: * @param off The offset into the byte array at which bytes will start to be
139: * placed.
140: * @param len the maximum number of bytes that can be returned.
141: * @return The number of bytes returned or -1 if the end of stream has been
142: * reached.
143: * @see java.io.InputStream#read(byte[], int, int)
144: * @throws IOException if an IO problem occurs.
145: */
146: public int read(byte[] b, int off, int len) throws IOException {
147:
148: if (closed) {
149: throw new IOException("Attempted read from closed stream.");
150: }
151:
152: if (eof) {
153: return -1;
154: }
155: if (pos >= chunkSize) {
156: nextChunk();
157: if (eof) {
158: return -1;
159: }
160: }
161: len = Math.min(len, chunkSize - pos);
162: int count = in.read(b, off, len);
163: pos += count;
164: return count;
165: }
166:
167: /**
168: * Read some bytes from the stream.
169: * @param b The byte array that will hold the contents from the stream.
170: * @return The number of bytes returned or -1 if the end of stream has been
171: * reached.
172: * @see java.io.InputStream#read(byte[])
173: * @throws IOException if an IO problem occurs.
174: */
175: public int read(byte[] b) throws IOException {
176: return read(b, 0, b.length);
177: }
178:
179: /**
180: * Read the next chunk.
181: * @throws IOException If an IO error occurs.
182: */
183: private void nextChunk() throws IOException {
184: chunkSize = getChunkSize();
185: if (chunkSize < 0) {
186: throw new MalformedChunkCodingException(
187: "Negative chunk size");
188: }
189: bof = false;
190: pos = 0;
191: if (chunkSize == 0) {
192: eof = true;
193: parseTrailerHeaders();
194: }
195: }
196:
197: /**
198: * Expects the stream to start with a chunksize in hex with optional
199: * comments after a semicolon. The line must end with a CRLF: "a3; some
200: * comment\r\n" Positions the stream at the start of the next line.
201: *
202: * @param in The new input stream.
203: * @param required <tt>true<tt/> if a valid chunk must be present,
204: * <tt>false<tt/> otherwise.
205: *
206: * @return the chunk size as integer
207: *
208: * @throws IOException when the chunk size could not be parsed
209: */
210: private int getChunkSize() throws IOException {
211: // skip CRLF
212: if (!bof) {
213: int cr = in.read();
214: int lf = in.read();
215: if ((cr != HTTP.CR) || (lf != HTTP.LF)) {
216: throw new MalformedChunkCodingException(
217: "CRLF expected at end of chunk");
218: }
219: }
220: //parse data
221: this .buffer.clear();
222: int i = this .in.readLine(this .buffer);
223: if (i == -1) {
224: throw new MalformedChunkCodingException(
225: "Chunked stream ended unexpectedly");
226: }
227: int separator = this .buffer.indexOf(';');
228: if (separator < 0) {
229: separator = this .buffer.length();
230: }
231: try {
232: return Integer.parseInt(this .buffer.substringTrimmed(0,
233: separator), 16);
234: } catch (NumberFormatException e) {
235: throw new MalformedChunkCodingException("Bad chunk header");
236: }
237: }
238:
239: /**
240: * Reads and stores the Trailer headers.
241: * @throws IOException If an IO problem occurs
242: */
243: private void parseTrailerHeaders() throws IOException {
244: try {
245: this .footers = AbstractMessageParser.parseHeaders(in, -1,
246: -1, null);
247: } catch (HttpException e) {
248: IOException ioe = new MalformedChunkCodingException(
249: "Invalid footer: " + e.getMessage());
250: ExceptionUtils.initCause(ioe, e);
251: throw ioe;
252: }
253: }
254:
255: /**
256: * Upon close, this reads the remainder of the chunked message,
257: * leaving the underlying socket at a position to start reading the
258: * next response without scanning.
259: * @throws IOException If an IO problem occurs.
260: */
261: public void close() throws IOException {
262: if (!closed) {
263: try {
264: if (!eof) {
265: exhaustInputStream(this );
266: }
267: } finally {
268: eof = true;
269: closed = true;
270: }
271: }
272: }
273:
274: public Header[] getFooters() {
275: return (Header[]) this .footers.clone();
276: }
277:
278: /**
279: * Exhaust an input stream, reading until EOF has been encountered.
280: *
281: * <p>Note that this function is intended as a non-public utility.
282: * This is a little weird, but it seemed silly to make a utility
283: * class for this one function, so instead it is just static and
284: * shared that way.</p>
285: *
286: * @param inStream The {@link InputStream} to exhaust.
287: * @throws IOException If an IO problem occurs
288: */
289: static void exhaustInputStream(final InputStream inStream)
290: throws IOException {
291: // read and discard the remainder of the message
292: byte buffer[] = new byte[1024];
293: while (inStream.read(buffer) >= 0) {
294: ;
295: }
296: }
297:
298: }
|