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