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.CharsetDecoder;
35  import java.nio.charset.CoderResult;
36  
37  import org.apache.http.MessageConstraintException;
38  import org.apache.http.annotation.NotThreadSafe;
39  import org.apache.http.config.MessageConstraints;
40  import org.apache.http.io.BufferInfo;
41  import org.apache.http.io.HttpTransportMetrics;
42  import org.apache.http.io.SessionInputBuffer;
43  import org.apache.http.protocol.HTTP;
44  import org.apache.http.util.Args;
45  import org.apache.http.util.Asserts;
46  import org.apache.http.util.ByteArrayBuffer;
47  import org.apache.http.util.CharArrayBuffer;
48  
49  /**
50   * Abstract base class for session input buffers that stream data from
51   * an arbitrary {@link InputStream}. This class buffers input data in
52   * an internal byte array for optimal input performance.
53   * <p/>
54   * {@link #readLine(CharArrayBuffer)} and {@link #readLine()} methods of this
55   * class treat a lone LF as valid line delimiters in addition to CR-LF required
56   * by the HTTP specification.
57   *
58   * @since 4.3
59   */
60  @NotThreadSafe
61  public class SessionInputBufferImpl implements SessionInputBuffer, BufferInfo {
62  
63      private final HttpTransportMetricsImpl metrics;
64      private final byte[] buffer;
65      private final ByteArrayBuffer linebuffer;
66      private final int minChunkLimit;
67      private final MessageConstraints constraints;
68      private final CharsetDecoder decoder;
69  
70      private InputStream instream;
71      private int bufferpos;
72      private int bufferlen;
73      private CharBuffer cbuf;
74  
75      /**
76       * Creates new instance of SessionInputBufferImpl.
77       *
78       * @param metrics HTTP transport metrics.
79       * @param buffersize buffer size. Must be a positive number.
80       * @param minChunkLimit size limit below which data chunks should be buffered in memory
81       *   in order to minimize native method invocations on the underlying network socket.
82       *   The optimal value of this parameter can be platform specific and defines a trade-off
83       *   between performance of memory copy operations and that of native method invocation.
84       *   If negative default chunk limited will be used.
85       * @param constraints Message constraints. If <code>null</code>
86       *   {@link MessageConstraints#DEFAULT} will be used.
87       * @param chardecoder chardecoder to be used for decoding HTTP protocol elements.
88       *   If <code>null</code> simple type cast will be used for byte to char conversion.
89       */
90      public SessionInputBufferImpl(
91              final HttpTransportMetricsImpl metrics,
92              final int buffersize,
93              final int minChunkLimit,
94              final MessageConstraints constraints,
95              final CharsetDecoder chardecoder) {
96          Args.notNull(metrics, "HTTP transport metrcis");
97          Args.positive(buffersize, "Buffer size");
98          this.metrics = metrics;
99          this.buffer = new byte[buffersize];
100         this.bufferpos = 0;
101         this.bufferlen = 0;
102         this.minChunkLimit = minChunkLimit >= 0 ? minChunkLimit : 512;
103         this.constraints = constraints != null ? constraints : MessageConstraints.DEFAULT;
104         this.linebuffer = new ByteArrayBuffer(buffersize);
105         this.decoder = chardecoder;
106     }
107 
108     public SessionInputBufferImpl(
109             final HttpTransportMetricsImpl metrics,
110             final int buffersize) {
111         this(metrics, buffersize, buffersize, null, null);
112     }
113 
114     public void bind(final InputStream instream) {
115         this.instream = instream;
116     }
117 
118     public boolean isBound() {
119         return this.instream != null;
120     }
121 
122     @Override
123     public int capacity() {
124         return this.buffer.length;
125     }
126 
127     @Override
128     public int length() {
129         return this.bufferlen - this.bufferpos;
130     }
131 
132     @Override
133     public int available() {
134         return capacity() - length();
135     }
136 
137     private int streamRead(final byte[] b, final int off, final int len) throws IOException {
138         Asserts.notNull(this.instream, "Input stream");
139         return this.instream.read(b, off, len);
140     }
141 
142     public int fillBuffer() throws IOException {
143         // compact the buffer if necessary
144         if (this.bufferpos > 0) {
145             final int len = this.bufferlen - this.bufferpos;
146             if (len > 0) {
147                 System.arraycopy(this.buffer, this.bufferpos, this.buffer, 0, len);
148             }
149             this.bufferpos = 0;
150             this.bufferlen = len;
151         }
152         final int l;
153         final int off = this.bufferlen;
154         final int len = this.buffer.length - off;
155         l = streamRead(this.buffer, off, len);
156         if (l == -1) {
157             return -1;
158         } else {
159             this.bufferlen = off + l;
160             this.metrics.incrementBytesTransferred(l);
161             return l;
162         }
163     }
164 
165     public boolean hasBufferedData() {
166         return this.bufferpos < this.bufferlen;
167     }
168 
169     public void clear() {
170         this.bufferpos = 0;
171         this.bufferlen = 0;
172     }
173 
174     @Override
175     public int read() throws IOException {
176         int noRead;
177         while (!hasBufferedData()) {
178             noRead = fillBuffer();
179             if (noRead == -1) {
180                 return -1;
181             }
182         }
183         return this.buffer[this.bufferpos++] & 0xff;
184     }
185 
186     @Override
187     public int read(final byte[] b, final int off, final int len) throws IOException {
188         if (b == null) {
189             return 0;
190         }
191         if (hasBufferedData()) {
192             final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
193             System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
194             this.bufferpos += chunk;
195             return chunk;
196         }
197         // If the remaining capacity is big enough, read directly from the
198         // underlying input stream bypassing the buffer.
199         if (len > this.minChunkLimit) {
200             final int read = streamRead(b, off, len);
201             if (read > 0) {
202                 this.metrics.incrementBytesTransferred(read);
203             }
204             return read;
205         } else {
206             // otherwise read to the buffer first
207             while (!hasBufferedData()) {
208                 final int noRead = fillBuffer();
209                 if (noRead == -1) {
210                     return -1;
211                 }
212             }
213             final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
214             System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
215             this.bufferpos += chunk;
216             return chunk;
217         }
218     }
219 
220     @Override
221     public int read(final byte[] b) throws IOException {
222         if (b == null) {
223             return 0;
224         }
225         return read(b, 0, b.length);
226     }
227 
228     /**
229      * Reads a complete line of characters up to a line delimiter from this
230      * session buffer into the given line buffer. The number of chars actually
231      * read is returned as an integer. The line delimiter itself is discarded.
232      * If no char is available because the end of the stream has been reached,
233      * the value <code>-1</code> is returned. This method blocks until input
234      * data is available, end of file is detected, or an exception is thrown.
235      * <p>
236      * This method treats a lone LF as a valid line delimiters in addition
237      * to CR-LF required by the HTTP specification.
238      *
239      * @param      charbuffer   the line buffer.
240      * @return     one line of characters
241      * @exception  IOException  if an I/O error occurs.
242      */
243     @Override
244     public int readLine(final CharArrayBuffer charbuffer) throws IOException {
245         Args.notNull(charbuffer, "Char array buffer");
246         final int maxLineLen = this.constraints.getMaxLineLength();
247         int noRead = 0;
248         boolean retry = true;
249         while (retry) {
250             // attempt to find end of line (LF)
251             int pos = -1;
252             for (int i = this.bufferpos; i < this.bufferlen; i++) {
253                 if (this.buffer[i] == HTTP.LF) {
254                     pos = i;
255                     break;
256                 }
257             }
258 
259             if (maxLineLen > 0) {
260                 final int currentLen = this.linebuffer.length()
261                         + (pos > 0 ? pos : this.bufferlen) - this.bufferpos;
262                 if (currentLen >= maxLineLen) {
263                     throw new MessageConstraintException("Maximum line length limit exceeded");
264                 }
265             }
266 
267             if (pos != -1) {
268                 // end of line found.
269                 if (this.linebuffer.isEmpty()) {
270                     // the entire line is preset in the read buffer
271                     return lineFromReadBuffer(charbuffer, pos);
272                 }
273                 retry = false;
274                 final int len = pos + 1 - this.bufferpos;
275                 this.linebuffer.append(this.buffer, this.bufferpos, len);
276                 this.bufferpos = pos + 1;
277             } else {
278                 // end of line not found
279                 if (hasBufferedData()) {
280                     final int len = this.bufferlen - this.bufferpos;
281                     this.linebuffer.append(this.buffer, this.bufferpos, len);
282                     this.bufferpos = this.bufferlen;
283                 }
284                 noRead = fillBuffer();
285                 if (noRead == -1) {
286                     retry = false;
287                 }
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.decoder == null) {
326             charbuffer.append(this.linebuffer, 0, len);
327         } else {
328             final 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, final int position)
336             throws IOException {
337         int pos = position;
338         final int off = this.bufferpos;
339         int len;
340         this.bufferpos = pos + 1;
341         if (pos > off && this.buffer[pos - 1] == HTTP.CR) {
342             // skip CR if found
343             pos--;
344         }
345         len = pos - off;
346         if (this.decoder == null) {
347             charbuffer.append(this.buffer, off, len);
348         } else {
349             final ByteBuffer bbuf =  ByteBuffer.wrap(this.buffer, off, len);
350             len = appendDecoded(charbuffer, bbuf);
351         }
352         return len;
353     }
354 
355     private int appendDecoded(
356             final CharArrayBuffer charbuffer, final ByteBuffer bbuf) throws IOException {
357         if (!bbuf.hasRemaining()) {
358             return 0;
359         }
360         if (this.cbuf == null) {
361             this.cbuf = CharBuffer.allocate(1024);
362         }
363         this.decoder.reset();
364         int len = 0;
365         while (bbuf.hasRemaining()) {
366             final CoderResult result = this.decoder.decode(bbuf, this.cbuf, true);
367             len += handleDecodingResult(result, charbuffer, bbuf);
368         }
369         final CoderResult result = this.decoder.flush(this.cbuf);
370         len += handleDecodingResult(result, charbuffer, bbuf);
371         this.cbuf.clear();
372         return len;
373     }
374 
375     private int handleDecodingResult(
376             final CoderResult result,
377             final CharArrayBuffer charbuffer,
378             final ByteBuffer bbuf) throws IOException {
379         if (result.isError()) {
380             result.throwException();
381         }
382         this.cbuf.flip();
383         final int len = this.cbuf.remaining();
384         while (this.cbuf.hasRemaining()) {
385             charbuffer.append(this.cbuf.get());
386         }
387         this.cbuf.compact();
388         return len;
389     }
390 
391     @Override
392     public String readLine() throws IOException {
393         final CharArrayBuffer charbuffer = new CharArrayBuffer(64);
394         final int l = readLine(charbuffer);
395         if (l != -1) {
396             return charbuffer.toString();
397         } else {
398             return null;
399         }
400     }
401 
402     @Override
403     public boolean isDataAvailable(final int timeout) throws IOException {
404         return hasBufferedData();
405     }
406 
407     @Override
408     public HttpTransportMetrics getMetrics() {
409         return this.metrics;
410     }
411 
412 }