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