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