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