001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.io;
029
030import java.io.IOException;
031import java.io.InputStream;
032import java.nio.ByteBuffer;
033import java.nio.CharBuffer;
034import java.nio.charset.CharsetDecoder;
035import java.nio.charset.CoderResult;
036
037import org.apache.http.MessageConstraintException;
038import org.apache.http.config.MessageConstraints;
039import org.apache.http.io.BufferInfo;
040import org.apache.http.io.HttpTransportMetrics;
041import org.apache.http.io.SessionInputBuffer;
042import org.apache.http.protocol.HTTP;
043import org.apache.http.util.Args;
044import org.apache.http.util.Asserts;
045import org.apache.http.util.ByteArrayBuffer;
046import org.apache.http.util.CharArrayBuffer;
047
048/**
049 * Abstract base class for session input buffers that stream data from
050 * an arbitrary {@link InputStream}. This class buffers input data in
051 * an internal byte array for optimal input performance.
052 * <p>
053 * {@link #readLine(CharArrayBuffer)} and {@link #readLine()} methods of this
054 * class treat a lone LF as valid line delimiters in addition to CR-LF required
055 * by the HTTP specification.
056 *
057 * @since 4.3
058 */
059public class SessionInputBufferImpl implements SessionInputBuffer, BufferInfo {
060
061    private final HttpTransportMetricsImpl metrics;
062    private final byte[] buffer;
063    private final ByteArrayBuffer linebuffer;
064    private final int minChunkLimit;
065    private final MessageConstraints constraints;
066    private final CharsetDecoder decoder;
067
068    private InputStream instream;
069    private int bufferpos;
070    private int bufferlen;
071    private CharBuffer cbuf;
072
073    /**
074     * Creates new instance of SessionInputBufferImpl.
075     *
076     * @param metrics HTTP transport metrics.
077     * @param buffersize buffer size. Must be a positive number.
078     * @param minChunkLimit size limit below which data chunks should be buffered in memory
079     *   in order to minimize native method invocations on the underlying network socket.
080     *   The optimal value of this parameter can be platform specific and defines a trade-off
081     *   between performance of memory copy operations and that of native method invocation.
082     *   If negative default chunk limited will be used.
083     * @param constraints Message constraints. If {@code null}
084     *   {@link MessageConstraints#DEFAULT} will be used.
085     * @param chardecoder chardecoder to be used for decoding HTTP protocol elements.
086     *   If {@code null} simple type cast will be used for byte to char conversion.
087     */
088    public SessionInputBufferImpl(
089            final HttpTransportMetricsImpl metrics,
090            final int buffersize,
091            final int minChunkLimit,
092            final MessageConstraints constraints,
093            final CharsetDecoder chardecoder) {
094        Args.notNull(metrics, "HTTP transport metrcis");
095        Args.positive(buffersize, "Buffer size");
096        this.metrics = metrics;
097        this.buffer = new byte[buffersize];
098        this.bufferpos = 0;
099        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 instream) {
113        this.instream = instream;
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 l;
151        final int off = this.bufferlen;
152        final int len = this.buffer.length - off;
153        l = streamRead(this.buffer, off, len);
154        if (l == -1) {
155            return -1;
156        } else {
157            this.bufferlen = off + l;
158            this.metrics.incrementBytesTransferred(l);
159            return l;
160        }
161    }
162
163    public boolean hasBufferedData() {
164        return this.bufferpos < this.bufferlen;
165    }
166
167    public void clear() {
168        this.bufferpos = 0;
169        this.bufferlen = 0;
170    }
171
172    @Override
173    public int read() throws IOException {
174        int noRead;
175        while (!hasBufferedData()) {
176            noRead = fillBuffer();
177            if (noRead == -1) {
178                return -1;
179            }
180        }
181        return this.buffer[this.bufferpos++] & 0xff;
182    }
183
184    @Override
185    public int read(final byte[] b, final int off, final int len) throws IOException {
186        if (b == null) {
187            return 0;
188        }
189        if (hasBufferedData()) {
190            final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
191            System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
192            this.bufferpos += chunk;
193            return chunk;
194        }
195        // If the remaining capacity is big enough, read directly from the
196        // underlying input stream bypassing the buffer.
197        if (len > this.minChunkLimit) {
198            final int read = streamRead(b, off, len);
199            if (read > 0) {
200                this.metrics.incrementBytesTransferred(read);
201            }
202            return read;
203        } else {
204            // otherwise read to the buffer first
205            while (!hasBufferedData()) {
206                final int noRead = fillBuffer();
207                if (noRead == -1) {
208                    return -1;
209                }
210            }
211            final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
212            System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
213            this.bufferpos += chunk;
214            return chunk;
215        }
216    }
217
218    @Override
219    public int read(final byte[] b) throws IOException {
220        if (b == null) {
221            return 0;
222        }
223        return read(b, 0, b.length);
224    }
225
226    /**
227     * Reads a complete line of characters up to a line delimiter from this
228     * session buffer into the given line buffer. The number of chars actually
229     * read is returned as an integer. The line delimiter itself is discarded.
230     * If no char is available because the end of the stream has been reached,
231     * the value {@code -1} is returned. This method blocks until input
232     * data is available, end of file is detected, or an exception is thrown.
233     * <p>
234     * This method treats a lone LF as a valid line delimiters in addition
235     * to CR-LF required by the HTTP specification.
236     *
237     * @param      charbuffer   the line buffer.
238     * @return     one line of characters
239     * @throws  IOException  if an I/O error occurs.
240     */
241    @Override
242    public int readLine(final CharArrayBuffer charbuffer) throws IOException {
243        Args.notNull(charbuffer, "Char array buffer");
244        final int maxLineLen = this.constraints.getMaxLineLength();
245        int noRead = 0;
246        boolean retry = true;
247        while (retry) {
248            // attempt to find end of line (LF)
249            int pos = -1;
250            for (int i = this.bufferpos; i < this.bufferlen; i++) {
251                if (this.buffer[i] == HTTP.LF) {
252                    pos = i;
253                    break;
254                }
255            }
256
257            if (maxLineLen > 0) {
258                final int currentLen = this.linebuffer.length()
259                        + (pos >= 0 ? pos : this.bufferlen) - this.bufferpos;
260                if (currentLen >= maxLineLen) {
261                    throw new MessageConstraintException("Maximum line length limit exceeded");
262                }
263            }
264
265            if (pos != -1) {
266                // end of line found.
267                if (this.linebuffer.isEmpty()) {
268                    // the entire line is preset in the read buffer
269                    return lineFromReadBuffer(charbuffer, pos);
270                }
271                retry = false;
272                final int len = pos + 1 - this.bufferpos;
273                this.linebuffer.append(this.buffer, this.bufferpos, len);
274                this.bufferpos = pos + 1;
275            } else {
276                // end of line not found
277                if (hasBufferedData()) {
278                    final int len = this.bufferlen - this.bufferpos;
279                    this.linebuffer.append(this.buffer, this.bufferpos, len);
280                    this.bufferpos = this.bufferlen;
281                }
282                noRead = fillBuffer();
283                if (noRead == -1) {
284                    retry = false;
285                }
286            }
287        }
288        if (noRead == -1 && this.linebuffer.isEmpty()) {
289            // indicate the end of stream
290            return -1;
291        }
292        return lineFromLineBuffer(charbuffer);
293    }
294
295    /**
296     * Reads a complete line of characters up to a line delimiter from this
297     * session buffer. The line delimiter itself is discarded. If no char is
298     * available because the end of the stream has been reached,
299     * {@code null} is returned. This method blocks until input data is
300     * available, end of file is detected, or an exception is thrown.
301     * <p>
302     * This method treats a lone LF as a valid line delimiters in addition
303     * to CR-LF required by the HTTP specification.
304     *
305     * @return HTTP line as a string
306     * @throws  IOException  if an I/O error occurs.
307     */
308    private int lineFromLineBuffer(final CharArrayBuffer charbuffer)
309            throws IOException {
310        // discard LF if found
311        int len = this.linebuffer.length();
312        if (len > 0) {
313            if (this.linebuffer.byteAt(len - 1) == HTTP.LF) {
314                len--;
315            }
316            // discard CR if found
317            if (len > 0) {
318                if (this.linebuffer.byteAt(len - 1) == HTTP.CR) {
319                    len--;
320                }
321            }
322        }
323        if (this.decoder == null) {
324            charbuffer.append(this.linebuffer, 0, len);
325        } else {
326            final ByteBuffer bbuf =  ByteBuffer.wrap(this.linebuffer.buffer(), 0, len);
327            len = appendDecoded(charbuffer, bbuf);
328        }
329        this.linebuffer.clear();
330        return len;
331    }
332
333    private int lineFromReadBuffer(final CharArrayBuffer charbuffer, final int position)
334            throws IOException {
335        int pos = position;
336        final int off = this.bufferpos;
337        int len;
338        this.bufferpos = pos + 1;
339        if (pos > off && this.buffer[pos - 1] == HTTP.CR) {
340            // skip CR if found
341            pos--;
342        }
343        len = pos - off;
344        if (this.decoder == null) {
345            charbuffer.append(this.buffer, off, len);
346        } else {
347            final ByteBuffer bbuf =  ByteBuffer.wrap(this.buffer, off, len);
348            len = appendDecoded(charbuffer, bbuf);
349        }
350        return len;
351    }
352
353    private int appendDecoded(
354            final CharArrayBuffer charbuffer, final ByteBuffer bbuf) throws IOException {
355        if (!bbuf.hasRemaining()) {
356            return 0;
357        }
358        if (this.cbuf == null) {
359            this.cbuf = CharBuffer.allocate(1024);
360        }
361        this.decoder.reset();
362        int len = 0;
363        while (bbuf.hasRemaining()) {
364            final CoderResult result = this.decoder.decode(bbuf, this.cbuf, true);
365            len += handleDecodingResult(result, charbuffer, bbuf);
366        }
367        final CoderResult result = this.decoder.flush(this.cbuf);
368        len += handleDecodingResult(result, charbuffer, bbuf);
369        this.cbuf.clear();
370        return len;
371    }
372
373    private int handleDecodingResult(
374            final CoderResult result,
375            final CharArrayBuffer charbuffer,
376            final ByteBuffer bbuf) throws IOException {
377        if (result.isError()) {
378            result.throwException();
379        }
380        this.cbuf.flip();
381        final int len = this.cbuf.remaining();
382        while (this.cbuf.hasRemaining()) {
383            charbuffer.append(this.cbuf.get());
384        }
385        this.cbuf.compact();
386        return len;
387    }
388
389    @Override
390    public String readLine() throws IOException {
391        final CharArrayBuffer charbuffer = new CharArrayBuffer(64);
392        final int l = readLine(charbuffer);
393        if (l != -1) {
394            return charbuffer.toString();
395        } else {
396            return null;
397        }
398    }
399
400    @Override
401    public boolean isDataAvailable(final int timeout) throws IOException {
402        return hasBufferedData();
403    }
404
405    @Override
406    public HttpTransportMetrics getMetrics() {
407        return this.metrics;
408    }
409
410}