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.hc.core5.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.hc.core5.http.Chars;
38  import org.apache.hc.core5.http.MessageConstraintException;
39  import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
40  import org.apache.hc.core5.http.io.HttpTransportMetrics;
41  import org.apache.hc.core5.http.io.SessionInputBuffer;
42  import org.apache.hc.core5.util.Args;
43  import org.apache.hc.core5.util.ByteArrayBuffer;
44  import org.apache.hc.core5.util.CharArrayBuffer;
45  
46  /**
47   * Abstract base class for session input buffers that stream data from
48   * an arbitrary {@link InputStream}. This class buffers input data in
49   * an internal byte array for optimal input performance.
50   * <p>
51   * {@link #readLine(CharArrayBuffer, InputStream)} method of this class treat a lone
52   * LF as valid line delimiters in addition to CR-LF required
53   * by the HTTP specification.
54   *
55   * @since 4.3
56   */
57  public class SessionInputBufferImpl implements SessionInputBuffer {
58  
59      private final BasicHttpTransportMetrics metrics;
60      private final byte[] buffer;
61      private final ByteArrayBuffer lineBuffer;
62      private final int minChunkLimit;
63      private final int maxLineLen;
64      private final CharsetDecoder decoder;
65  
66      private int bufferPos;
67      private int bufferLen;
68      private CharBuffer cbuf;
69  
70      /**
71       * Creates new instance of SessionInputBufferImpl.
72       *
73       * @param metrics HTTP transport metrics.
74       * @param bufferSize buffer size. Must be a positive number.
75       * @param minChunkLimit size limit below which data chunks should be buffered in memory
76       *   in order to minimize native method invocations on the underlying network socket.
77       *   The optimal value of this parameter can be platform specific and defines a trade-off
78       *   between performance of memory copy operations and that of native method invocation.
79       *   If negative default chunk limited will be used.
80       * @param maxLineLen maximum line length.
81       * @param charDecoder charDecoder to be used for decoding HTTP protocol elements.
82       *   If {@code null} simple type cast will be used for byte to char conversion.
83       */
84      public SessionInputBufferImpl(
85              final BasicHttpTransportMetrics metrics,
86              final int bufferSize,
87              final int minChunkLimit,
88              final int maxLineLen,
89              final CharsetDecoder charDecoder) {
90          Args.notNull(metrics, "HTTP transport metrics");
91          Args.positive(bufferSize, "Buffer size");
92          this.metrics = metrics;
93          this.buffer = new byte[bufferSize];
94          this.bufferPos = 0;
95          this.bufferLen = 0;
96          this.minChunkLimit = minChunkLimit >= 0 ? minChunkLimit : 512;
97          this.maxLineLen = maxLineLen > 0 ? maxLineLen : 0;
98          this.lineBuffer = new ByteArrayBuffer(bufferSize);
99          this.decoder = charDecoder;
100     }
101 
102     public SessionInputBufferImpl(
103             final BasicHttpTransportMetrics metrics,
104             final int bufferSize) {
105         this(metrics, bufferSize, bufferSize, 0, null);
106     }
107 
108     public SessionInputBufferImpl(final int bufferSize, final int maxLineLen) {
109         this(new BasicHttpTransportMetrics(), bufferSize, bufferSize, maxLineLen, null);
110     }
111 
112     public SessionInputBufferImpl(final int bufferSize, final CharsetDecoder decoder) {
113         this(new BasicHttpTransportMetrics(), bufferSize, bufferSize, 0, decoder);
114     }
115 
116     public SessionInputBufferImpl(final int bufferSize) {
117         this(new BasicHttpTransportMetrics(), bufferSize, bufferSize, 0, 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     public int fillBuffer(final InputStream inputStream) throws IOException {
136         Args.notNull(inputStream, "Input stream");
137         // compact the buffer if necessary
138         if (this.bufferPos > 0) {
139             final int len = this.bufferLen - this.bufferPos;
140             if (len > 0) {
141                 System.arraycopy(this.buffer, this.bufferPos, this.buffer, 0, len);
142             }
143             this.bufferPos = 0;
144             this.bufferLen = len;
145         }
146         final int readLen;
147         final int off = this.bufferLen;
148         final int len = this.buffer.length - off;
149         readLen = inputStream.read(this.buffer, off, len);
150         if (readLen == -1) {
151             return -1;
152         }
153         this.bufferLen = off + readLen;
154         this.metrics.incrementBytesTransferred(readLen);
155         return readLen;
156     }
157 
158     public boolean hasBufferedData() {
159         return this.bufferPos < this.bufferLen;
160     }
161 
162     public void clear() {
163         this.bufferPos = 0;
164         this.bufferLen = 0;
165     }
166 
167     @Override
168     public int read(final InputStream inputStream) throws IOException {
169         Args.notNull(inputStream, "Input stream");
170         int readLen;
171         while (!hasBufferedData()) {
172             readLen = fillBuffer(inputStream);
173             if (readLen == -1) {
174                 return -1;
175             }
176         }
177         return this.buffer[this.bufferPos++] & 0xff;
178     }
179 
180     @Override
181     public int read(final byte[] b, final int off, final int len, final InputStream inputStream) throws IOException {
182         Args.notNull(inputStream, "Input stream");
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 = inputStream.read(b, off, len);
196             if (read > 0) {
197                 this.metrics.incrementBytesTransferred(read);
198             }
199             return read;
200         }
201         // otherwise read to the buffer first
202         while (!hasBufferedData()) {
203             final int readLen = fillBuffer(inputStream);
204             if (readLen == -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     @Override
215     public int read(final byte[] b, final InputStream inputStream) throws IOException {
216         if (b == null) {
217             return 0;
218         }
219         return read(b, 0, b.length, inputStream);
220     }
221 
222     /**
223      * Reads a complete line of characters up to a line delimiter from this
224      * session buffer into the given line buffer. The number of chars actually
225      * read is returned as an integer. The line delimiter itself is discarded.
226      * If no char is available because the end of the stream has been reached,
227      * the value {@code -1} is returned. This method blocks until input
228      * data is available, end of file is detected, or an exception is thrown.
229      * <p>
230      * This method treats a lone LF as a valid line delimiters in addition
231      * to CR-LF required by the HTTP specification.
232      *
233      * @param     charBuffer   the line buffer, one line of characters upon return
234      * @return     the total number of bytes read into the buffer, or
235      *             {@code -1} is there is no more data because the end of
236      *             the stream has been reached.
237      * @throws  IOException  if an I/O error occurs.
238      */
239     @Override
240     public int readLine(final CharArrayBuffer charBuffer, final InputStream inputStream) throws IOException {
241         Args.notNull(charBuffer, "Char array buffer");
242         Args.notNull(inputStream, "Input stream");
243         int readLen = 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] == Chars.LF) {
250                     pos = i;
251                     break;
252                 }
253             }
254 
255             if (this.maxLineLen > 0) {
256                 final int currentLen = this.lineBuffer.length()
257                         + (pos >= 0 ? pos : this.bufferLen) - this.bufferPos;
258                 if (currentLen >= this.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                 readLen = fillBuffer(inputStream);
281                 if (readLen == -1) {
282                     retry = false;
283                 }
284             }
285         }
286         if (readLen == -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) == Chars.LF) {
312                 len--;
313             }
314             // discard CR if found
315             if (len > 0 && this.lineBuffer.byteAt(len - 1) == Chars.CR) {
316                 len--;
317             }
318         }
319         if (this.decoder == null) {
320             charBuffer.append(this.lineBuffer, 0, len);
321         } else {
322             final ByteBuffer bbuf =  ByteBuffer.wrap(this.lineBuffer.array(), 0, len);
323             len = appendDecoded(charBuffer, bbuf);
324         }
325         this.lineBuffer.clear();
326         return len;
327     }
328 
329     private int lineFromReadBuffer(final CharArrayBuffer charbuffer, final int position)
330             throws IOException {
331         int pos = position;
332         final int off = this.bufferPos;
333         int len;
334         this.bufferPos = pos + 1;
335         if (pos > off && this.buffer[pos - 1] == Chars.CR) {
336             // skip CR if found
337             pos--;
338         }
339         len = pos - off;
340         if (this.decoder == null) {
341             charbuffer.append(this.buffer, off, len);
342         } else {
343             final ByteBuffer bbuf =  ByteBuffer.wrap(this.buffer, off, len);
344             len = appendDecoded(charbuffer, bbuf);
345         }
346         return len;
347     }
348 
349     private int appendDecoded(
350             final CharArrayBuffer charbuffer, final ByteBuffer bbuf) throws IOException {
351         if (!bbuf.hasRemaining()) {
352             return 0;
353         }
354         if (this.cbuf == null) {
355             this.cbuf = CharBuffer.allocate(1024);
356         }
357         this.decoder.reset();
358         int len = 0;
359         while (bbuf.hasRemaining()) {
360             final CoderResult result = this.decoder.decode(bbuf, this.cbuf, true);
361             len += handleDecodingResult(result, charbuffer);
362         }
363         final CoderResult result = this.decoder.flush(this.cbuf);
364         len += handleDecodingResult(result, charbuffer);
365         this.cbuf.clear();
366         return len;
367     }
368 
369     private int handleDecodingResult(
370             final CoderResult result,
371             final CharArrayBuffer charBuffer) throws IOException {
372         if (result.isError()) {
373             result.throwException();
374         }
375         this.cbuf.flip();
376         final int len = this.cbuf.remaining();
377         while (this.cbuf.hasRemaining()) {
378             charBuffer.append(this.cbuf.get());
379         }
380         this.cbuf.compact();
381         return len;
382     }
383 
384     @Override
385     public HttpTransportMetrics getMetrics() {
386         return this.metrics;
387     }
388 
389 }