michael@0: /* michael@0: * ==================================================================== michael@0: * Licensed to the Apache Software Foundation (ASF) under one michael@0: * or more contributor license agreements. See the NOTICE file michael@0: * distributed with this work for additional information michael@0: * regarding copyright ownership. The ASF licenses this file michael@0: * to you under the Apache License, Version 2.0 (the michael@0: * "License"); you may not use this file except in compliance michael@0: * with the License. You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, michael@0: * software distributed under the License is distributed on an michael@0: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY michael@0: * KIND, either express or implied. See the License for the michael@0: * specific language governing permissions and limitations michael@0: * under the License. michael@0: * ==================================================================== michael@0: * michael@0: * This software consists of voluntary contributions made by many michael@0: * individuals on behalf of the Apache Software Foundation. For more michael@0: * information on the Apache Software Foundation, please see michael@0: * . michael@0: * michael@0: */ michael@0: michael@0: package ch.boye.httpclientandroidlib.impl.io; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: michael@0: import ch.boye.httpclientandroidlib.io.BufferInfo; michael@0: import ch.boye.httpclientandroidlib.io.SessionInputBuffer; michael@0: import ch.boye.httpclientandroidlib.io.HttpTransportMetrics; michael@0: import ch.boye.httpclientandroidlib.params.CoreConnectionPNames; michael@0: import ch.boye.httpclientandroidlib.params.HttpParams; michael@0: import ch.boye.httpclientandroidlib.params.HttpProtocolParams; michael@0: import ch.boye.httpclientandroidlib.protocol.HTTP; michael@0: import ch.boye.httpclientandroidlib.util.ByteArrayBuffer; michael@0: import ch.boye.httpclientandroidlib.util.CharArrayBuffer; michael@0: michael@0: /** michael@0: * Abstract base class for session input buffers that stream data from michael@0: * an arbitrary {@link InputStream}. This class buffers input data in michael@0: * an internal byte array for optimal input performance. michael@0: *

michael@0: * {@link #readLine(CharArrayBuffer)} and {@link #readLine()} methods of this michael@0: * class treat a lone LF as valid line delimiters in addition to CR-LF required michael@0: * by the HTTP specification. michael@0: * michael@0: *

michael@0: * The following parameters can be used to customize the behavior of this michael@0: * class: michael@0: *

michael@0: * @since 4.0 michael@0: */ michael@0: public abstract class AbstractSessionInputBuffer implements SessionInputBuffer, BufferInfo { michael@0: michael@0: private InputStream instream; michael@0: private byte[] buffer; michael@0: private int bufferpos; michael@0: private int bufferlen; michael@0: michael@0: private ByteArrayBuffer linebuffer = null; michael@0: michael@0: private String charset = HTTP.US_ASCII; michael@0: private boolean ascii = true; michael@0: private int maxLineLen = -1; michael@0: private int minChunkLimit = 512; michael@0: michael@0: private HttpTransportMetricsImpl metrics; michael@0: michael@0: /** michael@0: * Initializes this session input buffer. michael@0: * michael@0: * @param instream the source input stream. michael@0: * @param buffersize the size of the internal buffer. michael@0: * @param params HTTP parameters. michael@0: */ michael@0: protected void init(final InputStream instream, int buffersize, final HttpParams params) { michael@0: if (instream == null) { michael@0: throw new IllegalArgumentException("Input stream may not be null"); michael@0: } michael@0: if (buffersize <= 0) { michael@0: throw new IllegalArgumentException("Buffer size may not be negative or zero"); michael@0: } michael@0: if (params == null) { michael@0: throw new IllegalArgumentException("HTTP parameters may not be null"); michael@0: } michael@0: this.instream = instream; michael@0: this.buffer = new byte[buffersize]; michael@0: this.bufferpos = 0; michael@0: this.bufferlen = 0; michael@0: this.linebuffer = new ByteArrayBuffer(buffersize); michael@0: this.charset = HttpProtocolParams.getHttpElementCharset(params); michael@0: this.ascii = this.charset.equalsIgnoreCase(HTTP.US_ASCII) michael@0: || this.charset.equalsIgnoreCase(HTTP.ASCII); michael@0: this.maxLineLen = params.getIntParameter(CoreConnectionPNames.MAX_LINE_LENGTH, -1); michael@0: this.minChunkLimit = params.getIntParameter(CoreConnectionPNames.MIN_CHUNK_LIMIT, 512); michael@0: this.metrics = createTransportMetrics(); michael@0: } michael@0: michael@0: /** michael@0: * @since 4.1 michael@0: */ michael@0: protected HttpTransportMetricsImpl createTransportMetrics() { michael@0: return new HttpTransportMetricsImpl(); michael@0: } michael@0: michael@0: /** michael@0: * @since 4.1 michael@0: */ michael@0: public int capacity() { michael@0: return this.buffer.length; michael@0: } michael@0: michael@0: /** michael@0: * @since 4.1 michael@0: */ michael@0: public int length() { michael@0: return this.bufferlen - this.bufferpos; michael@0: } michael@0: michael@0: /** michael@0: * @since 4.1 michael@0: */ michael@0: public int available() { michael@0: return capacity() - length(); michael@0: } michael@0: michael@0: protected int fillBuffer() throws IOException { michael@0: // compact the buffer if necessary michael@0: if (this.bufferpos > 0) { michael@0: int len = this.bufferlen - this.bufferpos; michael@0: if (len > 0) { michael@0: System.arraycopy(this.buffer, this.bufferpos, this.buffer, 0, len); michael@0: } michael@0: this.bufferpos = 0; michael@0: this.bufferlen = len; michael@0: } michael@0: int l; michael@0: int off = this.bufferlen; michael@0: int len = this.buffer.length - off; michael@0: l = this.instream.read(this.buffer, off, len); michael@0: if (l == -1) { michael@0: return -1; michael@0: } else { michael@0: this.bufferlen = off + l; michael@0: this.metrics.incrementBytesTransferred(l); michael@0: return l; michael@0: } michael@0: } michael@0: michael@0: protected boolean hasBufferedData() { michael@0: return this.bufferpos < this.bufferlen; michael@0: } michael@0: michael@0: public int read() throws IOException { michael@0: int noRead = 0; michael@0: while (!hasBufferedData()) { michael@0: noRead = fillBuffer(); michael@0: if (noRead == -1) { michael@0: return -1; michael@0: } michael@0: } michael@0: return this.buffer[this.bufferpos++] & 0xff; michael@0: } michael@0: michael@0: public int read(final byte[] b, int off, int len) throws IOException { michael@0: if (b == null) { michael@0: return 0; michael@0: } michael@0: if (hasBufferedData()) { michael@0: int chunk = Math.min(len, this.bufferlen - this.bufferpos); michael@0: System.arraycopy(this.buffer, this.bufferpos, b, off, chunk); michael@0: this.bufferpos += chunk; michael@0: return chunk; michael@0: } michael@0: // If the remaining capacity is big enough, read directly from the michael@0: // underlying input stream bypassing the buffer. michael@0: if (len > this.minChunkLimit) { michael@0: int read = this.instream.read(b, off, len); michael@0: if (read > 0) { michael@0: this.metrics.incrementBytesTransferred(read); michael@0: } michael@0: return read; michael@0: } else { michael@0: // otherwise read to the buffer first michael@0: while (!hasBufferedData()) { michael@0: int noRead = fillBuffer(); michael@0: if (noRead == -1) { michael@0: return -1; michael@0: } michael@0: } michael@0: int chunk = Math.min(len, this.bufferlen - this.bufferpos); michael@0: System.arraycopy(this.buffer, this.bufferpos, b, off, chunk); michael@0: this.bufferpos += chunk; michael@0: return chunk; michael@0: } michael@0: } michael@0: michael@0: public int read(final byte[] b) throws IOException { michael@0: if (b == null) { michael@0: return 0; michael@0: } michael@0: return read(b, 0, b.length); michael@0: } michael@0: michael@0: private int locateLF() { michael@0: for (int i = this.bufferpos; i < this.bufferlen; i++) { michael@0: if (this.buffer[i] == HTTP.LF) { michael@0: return i; michael@0: } michael@0: } michael@0: return -1; michael@0: } michael@0: michael@0: /** michael@0: * Reads a complete line of characters up to a line delimiter from this michael@0: * session buffer into the given line buffer. The number of chars actually michael@0: * read is returned as an integer. The line delimiter itself is discarded. michael@0: * If no char is available because the end of the stream has been reached, michael@0: * the value -1 is returned. This method blocks until input michael@0: * data is available, end of file is detected, or an exception is thrown. michael@0: *

michael@0: * This method treats a lone LF as a valid line delimiters in addition michael@0: * to CR-LF required by the HTTP specification. michael@0: * michael@0: * @param charbuffer the line buffer. michael@0: * @return one line of characters michael@0: * @exception IOException if an I/O error occurs. michael@0: */ michael@0: public int readLine(final CharArrayBuffer charbuffer) throws IOException { michael@0: if (charbuffer == null) { michael@0: throw new IllegalArgumentException("Char array buffer may not be null"); michael@0: } michael@0: int noRead = 0; michael@0: boolean retry = true; michael@0: while (retry) { michael@0: // attempt to find end of line (LF) michael@0: int i = locateLF(); michael@0: if (i != -1) { michael@0: // end of line found. michael@0: if (this.linebuffer.isEmpty()) { michael@0: // the entire line is preset in the read buffer michael@0: return lineFromReadBuffer(charbuffer, i); michael@0: } michael@0: retry = false; michael@0: int len = i + 1 - this.bufferpos; michael@0: this.linebuffer.append(this.buffer, this.bufferpos, len); michael@0: this.bufferpos = i + 1; michael@0: } else { michael@0: // end of line not found michael@0: if (hasBufferedData()) { michael@0: int len = this.bufferlen - this.bufferpos; michael@0: this.linebuffer.append(this.buffer, this.bufferpos, len); michael@0: this.bufferpos = this.bufferlen; michael@0: } michael@0: noRead = fillBuffer(); michael@0: if (noRead == -1) { michael@0: retry = false; michael@0: } michael@0: } michael@0: if (this.maxLineLen > 0 && this.linebuffer.length() >= this.maxLineLen) { michael@0: throw new IOException("Maximum line length limit exceeded"); michael@0: } michael@0: } michael@0: if (noRead == -1 && this.linebuffer.isEmpty()) { michael@0: // indicate the end of stream michael@0: return -1; michael@0: } michael@0: return lineFromLineBuffer(charbuffer); michael@0: } michael@0: michael@0: /** michael@0: * Reads a complete line of characters up to a line delimiter from this michael@0: * session buffer. The line delimiter itself is discarded. If no char is michael@0: * available because the end of the stream has been reached, michael@0: * null is returned. This method blocks until input data is michael@0: * available, end of file is detected, or an exception is thrown. michael@0: *

michael@0: * This method treats a lone LF as a valid line delimiters in addition michael@0: * to CR-LF required by the HTTP specification. michael@0: * michael@0: * @return HTTP line as a string michael@0: * @exception IOException if an I/O error occurs. michael@0: */ michael@0: private int lineFromLineBuffer(final CharArrayBuffer charbuffer) michael@0: throws IOException { michael@0: // discard LF if found michael@0: int l = this.linebuffer.length(); michael@0: if (l > 0) { michael@0: if (this.linebuffer.byteAt(l - 1) == HTTP.LF) { michael@0: l--; michael@0: this.linebuffer.setLength(l); michael@0: } michael@0: // discard CR if found michael@0: if (l > 0) { michael@0: if (this.linebuffer.byteAt(l - 1) == HTTP.CR) { michael@0: l--; michael@0: this.linebuffer.setLength(l); michael@0: } michael@0: } michael@0: } michael@0: l = this.linebuffer.length(); michael@0: if (this.ascii) { michael@0: charbuffer.append(this.linebuffer, 0, l); michael@0: } else { michael@0: // This is VERY memory inefficient, BUT since non-ASCII charsets are michael@0: // NOT meant to be used anyway, there's no point optimizing it michael@0: String s = new String(this.linebuffer.buffer(), 0, l, this.charset); michael@0: l = s.length(); michael@0: charbuffer.append(s); michael@0: } michael@0: this.linebuffer.clear(); michael@0: return l; michael@0: } michael@0: michael@0: private int lineFromReadBuffer(final CharArrayBuffer charbuffer, int pos) michael@0: throws IOException { michael@0: int off = this.bufferpos; michael@0: int len; michael@0: this.bufferpos = pos + 1; michael@0: if (pos > 0 && this.buffer[pos - 1] == HTTP.CR) { michael@0: // skip CR if found michael@0: pos--; michael@0: } michael@0: len = pos - off; michael@0: if (this.ascii) { michael@0: charbuffer.append(this.buffer, off, len); michael@0: } else { michael@0: // This is VERY memory inefficient, BUT since non-ASCII charsets are michael@0: // NOT meant to be used anyway, there's no point optimizing it michael@0: String s = new String(this.buffer, off, len, this.charset); michael@0: charbuffer.append(s); michael@0: len = s.length(); michael@0: } michael@0: return len; michael@0: } michael@0: michael@0: public String readLine() throws IOException { michael@0: CharArrayBuffer charbuffer = new CharArrayBuffer(64); michael@0: int l = readLine(charbuffer); michael@0: if (l != -1) { michael@0: return charbuffer.toString(); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public HttpTransportMetrics getMetrics() { michael@0: return this.metrics; michael@0: } michael@0: michael@0: }