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, nullnull);
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 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 }
405