|
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.io.BufferInfo; |
|
34 import ch.boye.httpclientandroidlib.io.SessionInputBuffer; |
|
35 import ch.boye.httpclientandroidlib.io.HttpTransportMetrics; |
|
36 import ch.boye.httpclientandroidlib.params.CoreConnectionPNames; |
|
37 import ch.boye.httpclientandroidlib.params.HttpParams; |
|
38 import ch.boye.httpclientandroidlib.params.HttpProtocolParams; |
|
39 import ch.boye.httpclientandroidlib.protocol.HTTP; |
|
40 import ch.boye.httpclientandroidlib.util.ByteArrayBuffer; |
|
41 import ch.boye.httpclientandroidlib.util.CharArrayBuffer; |
|
42 |
|
43 /** |
|
44 * Abstract base class for session input buffers that stream data from |
|
45 * an arbitrary {@link InputStream}. This class buffers input data in |
|
46 * an internal byte array for optimal input performance. |
|
47 * <p> |
|
48 * {@link #readLine(CharArrayBuffer)} and {@link #readLine()} methods of this |
|
49 * class treat a lone LF as valid line delimiters in addition to CR-LF required |
|
50 * by the HTTP specification. |
|
51 * |
|
52 * <p> |
|
53 * The following parameters can be used to customize the behavior of this |
|
54 * class: |
|
55 * <ul> |
|
56 * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET}</li> |
|
57 * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li> |
|
58 * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MIN_CHUNK_LIMIT}</li> |
|
59 * </ul> |
|
60 * @since 4.0 |
|
61 */ |
|
62 public abstract class AbstractSessionInputBuffer implements SessionInputBuffer, BufferInfo { |
|
63 |
|
64 private InputStream instream; |
|
65 private byte[] buffer; |
|
66 private int bufferpos; |
|
67 private int bufferlen; |
|
68 |
|
69 private ByteArrayBuffer linebuffer = null; |
|
70 |
|
71 private String charset = HTTP.US_ASCII; |
|
72 private boolean ascii = true; |
|
73 private int maxLineLen = -1; |
|
74 private int minChunkLimit = 512; |
|
75 |
|
76 private HttpTransportMetricsImpl metrics; |
|
77 |
|
78 /** |
|
79 * Initializes this session input buffer. |
|
80 * |
|
81 * @param instream the source input stream. |
|
82 * @param buffersize the size of the internal buffer. |
|
83 * @param params HTTP parameters. |
|
84 */ |
|
85 protected void init(final InputStream instream, int buffersize, final HttpParams params) { |
|
86 if (instream == null) { |
|
87 throw new IllegalArgumentException("Input stream may not be null"); |
|
88 } |
|
89 if (buffersize <= 0) { |
|
90 throw new IllegalArgumentException("Buffer size may not be negative or zero"); |
|
91 } |
|
92 if (params == null) { |
|
93 throw new IllegalArgumentException("HTTP parameters may not be null"); |
|
94 } |
|
95 this.instream = instream; |
|
96 this.buffer = new byte[buffersize]; |
|
97 this.bufferpos = 0; |
|
98 this.bufferlen = 0; |
|
99 this.linebuffer = new ByteArrayBuffer(buffersize); |
|
100 this.charset = HttpProtocolParams.getHttpElementCharset(params); |
|
101 this.ascii = this.charset.equalsIgnoreCase(HTTP.US_ASCII) |
|
102 || this.charset.equalsIgnoreCase(HTTP.ASCII); |
|
103 this.maxLineLen = params.getIntParameter(CoreConnectionPNames.MAX_LINE_LENGTH, -1); |
|
104 this.minChunkLimit = params.getIntParameter(CoreConnectionPNames.MIN_CHUNK_LIMIT, 512); |
|
105 this.metrics = createTransportMetrics(); |
|
106 } |
|
107 |
|
108 /** |
|
109 * @since 4.1 |
|
110 */ |
|
111 protected HttpTransportMetricsImpl createTransportMetrics() { |
|
112 return new HttpTransportMetricsImpl(); |
|
113 } |
|
114 |
|
115 /** |
|
116 * @since 4.1 |
|
117 */ |
|
118 public int capacity() { |
|
119 return this.buffer.length; |
|
120 } |
|
121 |
|
122 /** |
|
123 * @since 4.1 |
|
124 */ |
|
125 public int length() { |
|
126 return this.bufferlen - this.bufferpos; |
|
127 } |
|
128 |
|
129 /** |
|
130 * @since 4.1 |
|
131 */ |
|
132 public int available() { |
|
133 return capacity() - length(); |
|
134 } |
|
135 |
|
136 protected int fillBuffer() throws IOException { |
|
137 // compact the buffer if necessary |
|
138 if (this.bufferpos > 0) { |
|
139 int len = this.bufferlen - this.bufferpos; |
|
140 if (len > 0) { |
|
141 System.arraycopy(this.buffer, this.bufferpos, this.buffer, 0, len); |
|
142 } |
|
143 this.bufferpos = 0; |
|
144 this.bufferlen = len; |
|
145 } |
|
146 int l; |
|
147 int off = this.bufferlen; |
|
148 int len = this.buffer.length - off; |
|
149 l = this.instream.read(this.buffer, off, len); |
|
150 if (l == -1) { |
|
151 return -1; |
|
152 } else { |
|
153 this.bufferlen = off + l; |
|
154 this.metrics.incrementBytesTransferred(l); |
|
155 return l; |
|
156 } |
|
157 } |
|
158 |
|
159 protected boolean hasBufferedData() { |
|
160 return this.bufferpos < this.bufferlen; |
|
161 } |
|
162 |
|
163 public int read() throws IOException { |
|
164 int noRead = 0; |
|
165 while (!hasBufferedData()) { |
|
166 noRead = fillBuffer(); |
|
167 if (noRead == -1) { |
|
168 return -1; |
|
169 } |
|
170 } |
|
171 return this.buffer[this.bufferpos++] & 0xff; |
|
172 } |
|
173 |
|
174 public int read(final byte[] b, int off, int len) throws IOException { |
|
175 if (b == null) { |
|
176 return 0; |
|
177 } |
|
178 if (hasBufferedData()) { |
|
179 int chunk = Math.min(len, this.bufferlen - this.bufferpos); |
|
180 System.arraycopy(this.buffer, this.bufferpos, b, off, chunk); |
|
181 this.bufferpos += chunk; |
|
182 return chunk; |
|
183 } |
|
184 // If the remaining capacity is big enough, read directly from the |
|
185 // underlying input stream bypassing the buffer. |
|
186 if (len > this.minChunkLimit) { |
|
187 int read = this.instream.read(b, off, len); |
|
188 if (read > 0) { |
|
189 this.metrics.incrementBytesTransferred(read); |
|
190 } |
|
191 return read; |
|
192 } else { |
|
193 // otherwise read to the buffer first |
|
194 while (!hasBufferedData()) { |
|
195 int noRead = fillBuffer(); |
|
196 if (noRead == -1) { |
|
197 return -1; |
|
198 } |
|
199 } |
|
200 int chunk = Math.min(len, this.bufferlen - this.bufferpos); |
|
201 System.arraycopy(this.buffer, this.bufferpos, b, off, chunk); |
|
202 this.bufferpos += chunk; |
|
203 return chunk; |
|
204 } |
|
205 } |
|
206 |
|
207 public int read(final byte[] b) throws IOException { |
|
208 if (b == null) { |
|
209 return 0; |
|
210 } |
|
211 return read(b, 0, b.length); |
|
212 } |
|
213 |
|
214 private int locateLF() { |
|
215 for (int i = this.bufferpos; i < this.bufferlen; i++) { |
|
216 if (this.buffer[i] == HTTP.LF) { |
|
217 return i; |
|
218 } |
|
219 } |
|
220 return -1; |
|
221 } |
|
222 |
|
223 /** |
|
224 * Reads a complete line of characters up to a line delimiter from this |
|
225 * session buffer into the given line buffer. The number of chars actually |
|
226 * read is returned as an integer. The line delimiter itself is discarded. |
|
227 * If no char is available because the end of the stream has been reached, |
|
228 * the value <code>-1</code> is returned. This method blocks until input |
|
229 * data is available, end of file is detected, or an exception is thrown. |
|
230 * <p> |
|
231 * This method treats a lone LF as a valid line delimiters in addition |
|
232 * to CR-LF required by the HTTP specification. |
|
233 * |
|
234 * @param charbuffer the line buffer. |
|
235 * @return one line of characters |
|
236 * @exception IOException if an I/O error occurs. |
|
237 */ |
|
238 public int readLine(final CharArrayBuffer charbuffer) throws IOException { |
|
239 if (charbuffer == null) { |
|
240 throw new IllegalArgumentException("Char array buffer may not be null"); |
|
241 } |
|
242 int noRead = 0; |
|
243 boolean retry = true; |
|
244 while (retry) { |
|
245 // attempt to find end of line (LF) |
|
246 int i = locateLF(); |
|
247 if (i != -1) { |
|
248 // end of line found. |
|
249 if (this.linebuffer.isEmpty()) { |
|
250 // the entire line is preset in the read buffer |
|
251 return lineFromReadBuffer(charbuffer, i); |
|
252 } |
|
253 retry = false; |
|
254 int len = i + 1 - this.bufferpos; |
|
255 this.linebuffer.append(this.buffer, this.bufferpos, len); |
|
256 this.bufferpos = i + 1; |
|
257 } else { |
|
258 // end of line not found |
|
259 if (hasBufferedData()) { |
|
260 int len = this.bufferlen - this.bufferpos; |
|
261 this.linebuffer.append(this.buffer, this.bufferpos, len); |
|
262 this.bufferpos = this.bufferlen; |
|
263 } |
|
264 noRead = fillBuffer(); |
|
265 if (noRead == -1) { |
|
266 retry = false; |
|
267 } |
|
268 } |
|
269 if (this.maxLineLen > 0 && this.linebuffer.length() >= this.maxLineLen) { |
|
270 throw new IOException("Maximum line length limit exceeded"); |
|
271 } |
|
272 } |
|
273 if (noRead == -1 && this.linebuffer.isEmpty()) { |
|
274 // indicate the end of stream |
|
275 return -1; |
|
276 } |
|
277 return lineFromLineBuffer(charbuffer); |
|
278 } |
|
279 |
|
280 /** |
|
281 * Reads a complete line of characters up to a line delimiter from this |
|
282 * session buffer. The line delimiter itself is discarded. If no char is |
|
283 * available because the end of the stream has been reached, |
|
284 * <code>null</code> is returned. This method blocks until input data is |
|
285 * available, end of file is detected, or an exception is thrown. |
|
286 * <p> |
|
287 * This method treats a lone LF as a valid line delimiters in addition |
|
288 * to CR-LF required by the HTTP specification. |
|
289 * |
|
290 * @return HTTP line as a string |
|
291 * @exception IOException if an I/O error occurs. |
|
292 */ |
|
293 private int lineFromLineBuffer(final CharArrayBuffer charbuffer) |
|
294 throws IOException { |
|
295 // discard LF if found |
|
296 int l = this.linebuffer.length(); |
|
297 if (l > 0) { |
|
298 if (this.linebuffer.byteAt(l - 1) == HTTP.LF) { |
|
299 l--; |
|
300 this.linebuffer.setLength(l); |
|
301 } |
|
302 // discard CR if found |
|
303 if (l > 0) { |
|
304 if (this.linebuffer.byteAt(l - 1) == HTTP.CR) { |
|
305 l--; |
|
306 this.linebuffer.setLength(l); |
|
307 } |
|
308 } |
|
309 } |
|
310 l = this.linebuffer.length(); |
|
311 if (this.ascii) { |
|
312 charbuffer.append(this.linebuffer, 0, l); |
|
313 } else { |
|
314 // This is VERY memory inefficient, BUT since non-ASCII charsets are |
|
315 // NOT meant to be used anyway, there's no point optimizing it |
|
316 String s = new String(this.linebuffer.buffer(), 0, l, this.charset); |
|
317 l = s.length(); |
|
318 charbuffer.append(s); |
|
319 } |
|
320 this.linebuffer.clear(); |
|
321 return l; |
|
322 } |
|
323 |
|
324 private int lineFromReadBuffer(final CharArrayBuffer charbuffer, int pos) |
|
325 throws IOException { |
|
326 int off = this.bufferpos; |
|
327 int len; |
|
328 this.bufferpos = pos + 1; |
|
329 if (pos > 0 && this.buffer[pos - 1] == HTTP.CR) { |
|
330 // skip CR if found |
|
331 pos--; |
|
332 } |
|
333 len = pos - off; |
|
334 if (this.ascii) { |
|
335 charbuffer.append(this.buffer, off, len); |
|
336 } else { |
|
337 // This is VERY memory inefficient, BUT since non-ASCII charsets are |
|
338 // NOT meant to be used anyway, there's no point optimizing it |
|
339 String s = new String(this.buffer, off, len, this.charset); |
|
340 charbuffer.append(s); |
|
341 len = s.length(); |
|
342 } |
|
343 return len; |
|
344 } |
|
345 |
|
346 public String readLine() throws IOException { |
|
347 CharArrayBuffer charbuffer = new CharArrayBuffer(64); |
|
348 int l = readLine(charbuffer); |
|
349 if (l != -1) { |
|
350 return charbuffer.toString(); |
|
351 } else { |
|
352 return null; |
|
353 } |
|
354 } |
|
355 |
|
356 public HttpTransportMetrics getMetrics() { |
|
357 return this.metrics; |
|
358 } |
|
359 |
|
360 } |