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