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 org.apache.http.impl.io;
29
30 import java.io.IOException;
31 import java.io.InputStream;
32
33 import org.apache.http.Header;
34 import org.apache.http.HttpException;
35 import org.apache.http.MalformedChunkCodingException;
36 import org.apache.http.TruncatedChunkException;
37 import org.apache.http.annotation.NotThreadSafe;
38 import org.apache.http.io.BufferInfo;
39 import org.apache.http.io.SessionInputBuffer;
40 import org.apache.http.util.CharArrayBuffer;
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 @NotThreadSafe
59 public class ChunkedInputStream extends InputStream {
60
61 private static final int CHUNK_LEN = 1;
62 private static final int CHUNK_DATA = 2;
63 private static final int CHUNK_CRLF = 3;
64
65 private static final int BUFFER_SIZE = 2048;
66
67 /** The session input buffer */
68 private final SessionInputBuffer in;
69
70 private final CharArrayBuffer buffer;
71
72 private int state;
73
74 /** The chunk size */
75 private int chunkSize;
76
77 /** The current position within the current chunk */
78 private int pos;
79
80 /** True if we've reached the end of stream */
81 private boolean eof = false;
82
83 /** True if this stream is closed */
84 private boolean closed = false;
85
86 private Header[] footers = new Header[] {};
87
88 /**
89 * Wraps session input stream and reads chunk coded input.
90 *
91 * @param in The session input buffer
92 */
93 public ChunkedInputStream(final SessionInputBuffer in) {
94 super();
95 if (in == null) {
96 throw new IllegalArgumentException("Session input buffer may not be null");
97 }
98 this.in = in;
99 this.pos = 0;
100 this.buffer = new CharArrayBuffer(16);
101 this.state = CHUNK_LEN;
102 }
103
104 @Override
105 public int available() throws IOException {
106 if (this.in instanceof BufferInfo) {
107 int len = ((BufferInfo) this.in).length();
108 return Math.min(len, this.chunkSize - this.pos);
109 } else {
110 return 0;
111 }
112 }
113
114 /**
115 * <p> Returns all the data in a chunked stream in coalesced form. A chunk
116 * is followed by a CRLF. The method returns -1 as soon as a chunksize of 0
117 * is detected.</p>
118 *
119 * <p> Trailer headers are read automatically at the end of the stream and
120 * can be obtained with the getResponseFooters() method.</p>
121 *
122 * @return -1 of the end of the stream has been reached or the next data
123 * byte
124 * @throws IOException in case of an I/O error
125 */
126 @Override
127 public int read() throws IOException {
128 if (this.closed) {
129 throw new IOException("Attempted read from closed stream.");
130 }
131 if (this.eof) {
132 return -1;
133 }
134 if (state != CHUNK_DATA) {
135 nextChunk();
136 if (this.eof) {
137 return -1;
138 }
139 }
140 int b = in.read();
141 if (b != -1) {
142 pos++;
143 if (pos >= chunkSize) {
144 state = CHUNK_CRLF;
145 }
146 }
147 return b;
148 }
149
150 /**
151 * Read some bytes from the stream.
152 * @param b The byte array that will hold the contents from the stream.
153 * @param off The offset into the byte array at which bytes will start to be
154 * placed.
155 * @param len the maximum number of bytes that can be returned.
156 * @return The number of bytes returned or -1 if the end of stream has been
157 * reached.
158 * @throws IOException in case of an I/O error
159 */
160 @Override
161 public int read (byte[] b, int off, int len) throws IOException {
162
163 if (closed) {
164 throw new IOException("Attempted read from closed stream.");
165 }
166
167 if (eof) {
168 return -1;
169 }
170 if (state != CHUNK_DATA) {
171 nextChunk();
172 if (eof) {
173 return -1;
174 }
175 }
176 len = Math.min(len, chunkSize - pos);
177 int bytesRead = in.read(b, off, len);
178 if (bytesRead != -1) {
179 pos += bytesRead;
180 if (pos >= chunkSize) {
181 state = CHUNK_CRLF;
182 }
183 return bytesRead;
184 } else {
185 eof = true;
186 throw new TruncatedChunkException("Truncated chunk "
187 + "( expected size: " + chunkSize
188 + "; actual size: " + pos + ")");
189 }
190 }
191
192 /**
193 * Read some bytes from the stream.
194 * @param b The byte array that will hold the contents from the stream.
195 * @return The number of bytes returned or -1 if the end of stream has been
196 * reached.
197 * @throws IOException in case of an I/O error
198 */
199 @Override
200 public int read (byte[] b) throws IOException {
201 return read(b, 0, b.length);
202 }
203
204 /**
205 * Read the next chunk.
206 * @throws IOException in case of an I/O error
207 */
208 private void nextChunk() throws IOException {
209 chunkSize = getChunkSize();
210 if (chunkSize < 0) {
211 throw new MalformedChunkCodingException("Negative chunk size");
212 }
213 state = CHUNK_DATA;
214 pos = 0;
215 if (chunkSize == 0) {
216 eof = true;
217 parseTrailerHeaders();
218 }
219 }
220
221 /**
222 * Expects the stream to start with a chunksize in hex with optional
223 * comments after a semicolon. The line must end with a CRLF: "a3; some
224 * comment\r\n" Positions the stream at the start of the next line.
225 *
226 * @param in The new input stream.
227 * @param required <tt>true<tt/> if a valid chunk must be present,
228 * <tt>false<tt/> otherwise.
229 *
230 * @return the chunk size as integer
231 *
232 * @throws IOException when the chunk size could not be parsed
233 */
234 private int getChunkSize() throws IOException {
235 int st = this.state;
236 switch (st) {
237 case CHUNK_CRLF:
238 this.buffer.clear();
239 int i = this.in.readLine(this.buffer);
240 if (i == -1) {
241 return 0;
242 }
243 if (!this.buffer.isEmpty()) {
244 throw new MalformedChunkCodingException(
245 "Unexpected content at the end of chunk");
246 }
247 state = CHUNK_LEN;
248 //$FALL-THROUGH$
249 case CHUNK_LEN:
250 this.buffer.clear();
251 i = this.in.readLine(this.buffer);
252 if (i == -1) {
253 return 0;
254 }
255 int separator = this.buffer.indexOf(';');
256 if (separator < 0) {
257 separator = this.buffer.length();
258 }
259 try {
260 return Integer.parseInt(this.buffer.substringTrimmed(0, separator), 16);
261 } catch (NumberFormatException e) {
262 throw new MalformedChunkCodingException("Bad chunk header");
263 }
264 default:
265 throw new IllegalStateException("Inconsistent codec state");
266 }
267 }
268
269 /**
270 * Reads and stores the Trailer headers.
271 * @throws IOException in case of an I/O error
272 */
273 private void parseTrailerHeaders() throws IOException {
274 try {
275 this.footers = AbstractMessageParser.parseHeaders
276 (in, -1, -1, null);
277 } catch (HttpException ex) {
278 IOException ioe = new MalformedChunkCodingException("Invalid footer: "
279 + ex.getMessage());
280 ioe.initCause(ex);
281 throw ioe;
282 }
283 }
284
285 /**
286 * Upon close, this reads the remainder of the chunked message,
287 * leaving the underlying socket at a position to start reading the
288 * next response without scanning.
289 * @throws IOException in case of an I/O error
290 */
291 @Override
292 public void close() throws IOException {
293 if (!closed) {
294 try {
295 if (!eof) {
296 // read and discard the remainder of the message
297 byte buffer[] = new byte[BUFFER_SIZE];
298 while (read(buffer) >= 0) {
299 }
300 }
301 } finally {
302 eof = true;
303 closed = true;
304 }
305 }
306 }
307
308 public Header[] getFooters() {
309 return this.footers.clone();
310 }
311
312 }