View Javadoc

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 }