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