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 void bind(final InputStream instream) {
109         this.instream = instream;
110     }
111 
112     public boolean isBound() {
113         return this.instream != null;
114     }
115 
116     public int capacity() {
117         return this.buffer.length;
118     }
119 
120     public int length() {
121         return this.bufferlen - this.bufferpos;
122     }
123 
124     public int available() {
125         return capacity() - length();
126     }
127 
128     private int streamRead(final byte[] b, final int off, final int len) throws IOException {
129         Asserts.notNull(this.instream, "Input stream");
130         return this.instream.read(b, off, len);
131     }
132 
133     public int fillBuffer() throws IOException {
134         // compact the buffer if necessary
135         if (this.bufferpos > 0) {
136             final int len = this.bufferlen - this.bufferpos;
137             if (len > 0) {
138                 System.arraycopy(this.buffer, this.bufferpos, this.buffer, 0, len);
139             }
140             this.bufferpos = 0;
141             this.bufferlen = len;
142         }
143         int l;
144         final int off = this.bufferlen;
145         final int len = this.buffer.length - off;
146         l = streamRead(this.buffer, off, len);
147         if (l == -1) {
148             return -1;
149         } else {
150             this.bufferlen = off + l;
151             this.metrics.incrementBytesTransferred(l);
152             return l;
153         }
154     }
155 
156     public boolean hasBufferedData() {
157         return this.bufferpos < this.bufferlen;
158     }
159 
160     public int read() throws IOException {
161         int noRead = 0;
162         while (!hasBufferedData()) {
163             noRead = fillBuffer();
164             if (noRead == -1) {
165                 return -1;
166             }
167         }
168         return this.buffer[this.bufferpos++] & 0xff;
169     }
170 
171     public int read(final byte[] b, final int off, final int len) throws IOException {
172         if (b == null) {
173             return 0;
174         }
175         if (hasBufferedData()) {
176             final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
177             System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
178             this.bufferpos += chunk;
179             return chunk;
180         }
181         // If the remaining capacity is big enough, read directly from the
182         // underlying input stream bypassing the buffer.
183         if (len > this.minChunkLimit) {
184             final int read = streamRead(b, off, len);
185             if (read > 0) {
186                 this.metrics.incrementBytesTransferred(read);
187             }
188             return read;
189         } else {
190             // otherwise read to the buffer first
191             while (!hasBufferedData()) {
192                 final int noRead = fillBuffer();
193                 if (noRead == -1) {
194                     return -1;
195                 }
196             }
197             final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
198             System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
199             this.bufferpos += chunk;
200             return chunk;
201         }
202     }
203 
204     public int read(final byte[] b) throws IOException {
205         if (b == null) {
206             return 0;
207         }
208         return read(b, 0, b.length);
209     }
210 
211     private int locateLF() {
212         for (int i = this.bufferpos; i < this.bufferlen; i++) {
213             if (this.buffer[i] == HTTP.LF) {
214                 return i;
215             }
216         }
217         return -1;
218     }
219 
220     /**
221      * Reads a complete line of characters up to a line delimiter from this
222      * session buffer into the given line buffer. The number of chars actually
223      * read is returned as an integer. The line delimiter itself is discarded.
224      * If no char is available because the end of the stream has been reached,
225      * the value <code>-1</code> is returned. This method blocks until input
226      * data is available, end of file is detected, or an exception is thrown.
227      * <p>
228      * This method treats a lone LF as a valid line delimiters in addition
229      * to CR-LF required by the HTTP specification.
230      *
231      * @param      charbuffer   the line buffer.
232      * @return     one line of characters
233      * @exception  IOException  if an I/O error occurs.
234      */
235     public int readLine(final CharArrayBuffer charbuffer) throws IOException {
236         Args.notNull(charbuffer, "Char array buffer");
237         int noRead = 0;
238         boolean retry = true;
239         while (retry) {
240             // attempt to find end of line (LF)
241             final int i = locateLF();
242             if (i != -1) {
243                 // end of line found.
244                 if (this.linebuffer.isEmpty()) {
245                     // the entire line is preset in the read buffer
246                     return lineFromReadBuffer(charbuffer, i);
247                 }
248                 retry = false;
249                 final int len = i + 1 - this.bufferpos;
250                 this.linebuffer.append(this.buffer, this.bufferpos, len);
251                 this.bufferpos = i + 1;
252             } else {
253                 // end of line not found
254                 if (hasBufferedData()) {
255                     final int len = this.bufferlen - this.bufferpos;
256                     this.linebuffer.append(this.buffer, this.bufferpos, len);
257                     this.bufferpos = this.bufferlen;
258                 }
259                 noRead = fillBuffer();
260                 if (noRead == -1) {
261                     retry = false;
262                 }
263             }
264             final int maxLineLen = this.constraints.getMaxLineLength();
265             if (maxLineLen > 0 && this.linebuffer.length() >= maxLineLen) {
266                 throw new MessageConstraintException("Maximum line length limit exceeded");
267             }
268         }
269         if (noRead == -1 && this.linebuffer.isEmpty()) {
270             // indicate the end of stream
271             return -1;
272         }
273         return lineFromLineBuffer(charbuffer);
274     }
275 
276     /**
277      * Reads a complete line of characters up to a line delimiter from this
278      * session buffer. The line delimiter itself is discarded. If no char is
279      * available because the end of the stream has been reached,
280      * <code>null</code> is returned. This method blocks until input data is
281      * available, end of file is detected, or an exception is thrown.
282      * <p>
283      * This method treats a lone LF as a valid line delimiters in addition
284      * to CR-LF required by the HTTP specification.
285      *
286      * @return HTTP line as a string
287      * @exception  IOException  if an I/O error occurs.
288      */
289     private int lineFromLineBuffer(final CharArrayBuffer charbuffer)
290             throws IOException {
291         // discard LF if found
292         int len = this.linebuffer.length();
293         if (len > 0) {
294             if (this.linebuffer.byteAt(len - 1) == HTTP.LF) {
295                 len--;
296             }
297             // discard CR if found
298             if (len > 0) {
299                 if (this.linebuffer.byteAt(len - 1) == HTTP.CR) {
300                     len--;
301                 }
302             }
303         }
304         if (this.decoder == null) {
305             charbuffer.append(this.linebuffer, 0, len);
306         } else {
307             final ByteBuffer bbuf =  ByteBuffer.wrap(this.linebuffer.buffer(), 0, len);
308             len = appendDecoded(charbuffer, bbuf);
309         }
310         this.linebuffer.clear();
311         return len;
312     }
313 
314     private int lineFromReadBuffer(final CharArrayBuffer charbuffer, int pos)
315             throws IOException {
316         final int off = this.bufferpos;
317         int len;
318         this.bufferpos = pos + 1;
319         if (pos > off && this.buffer[pos - 1] == HTTP.CR) {
320             // skip CR if found
321             pos--;
322         }
323         len = pos - off;
324         if (this.decoder == null) {
325             charbuffer.append(this.buffer, off, len);
326         } else {
327             final ByteBuffer bbuf =  ByteBuffer.wrap(this.buffer, off, len);
328             len = appendDecoded(charbuffer, bbuf);
329         }
330         return len;
331     }
332 
333     private int appendDecoded(
334             final CharArrayBuffer charbuffer, final ByteBuffer bbuf) throws IOException {
335         if (!bbuf.hasRemaining()) {
336             return 0;
337         }
338         if (this.cbuf == null) {
339             this.cbuf = CharBuffer.allocate(1024);
340         }
341         this.decoder.reset();
342         int len = 0;
343         while (bbuf.hasRemaining()) {
344             final CoderResult result = this.decoder.decode(bbuf, this.cbuf, true);
345             len += handleDecodingResult(result, charbuffer, bbuf);
346         }
347         final CoderResult result = this.decoder.flush(this.cbuf);
348         len += handleDecodingResult(result, charbuffer, bbuf);
349         this.cbuf.clear();
350         return len;
351     }
352 
353     private int handleDecodingResult(
354             final CoderResult result,
355             final CharArrayBuffer charbuffer,
356             final ByteBuffer bbuf) throws IOException {
357         if (result.isError()) {
358             result.throwException();
359         }
360         this.cbuf.flip();
361         final int len = this.cbuf.remaining();
362         while (this.cbuf.hasRemaining()) {
363             charbuffer.append(this.cbuf.get());
364         }
365         this.cbuf.compact();
366         return len;
367     }
368 
369     public String readLine() throws IOException {
370         final CharArrayBuffer charbuffer = new CharArrayBuffer(64);
371         final int l = readLine(charbuffer);
372         if (l != -1) {
373             return charbuffer.toString();
374         } else {
375             return null;
376         }
377     }
378 
379     public boolean isDataAvailable(final int timeout) throws IOException {
380         return hasBufferedData();
381     }
382 
383     public HttpTransportMetrics getMetrics() {
384         return this.metrics;
385     }
386 
387 }