All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glassfish.grizzly.http.io.InputBuffer Maven / Gradle / Ivy

There is a newer version: 4.0.2
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package org.glassfish.grizzly.http.io;

import org.glassfish.grizzly.http.HttpBrokenContent;
import org.glassfish.grizzly.http.HttpHeader;
import org.glassfish.grizzly.http.HttpTrailer;
import java.io.EOFException;
import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.ReadResult;
import org.glassfish.grizzly.ReadHandler;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.http.HttpContent;
import org.glassfish.grizzly.http.util.MimeHeaders;
import org.glassfish.grizzly.threadpool.Threads;
import org.glassfish.grizzly.utils.Charsets;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.http.HttpBrokenContentException;
import org.glassfish.grizzly.memory.Buffers;
import org.glassfish.grizzly.memory.CompositeBuffer;
import org.glassfish.grizzly.utils.Exceptions;

import static org.glassfish.grizzly.http.util.Constants.*;

/**
 * Abstraction exposing both byte and character methods to read content
 * from the HTTP messaging system in Grizzly.
 */
public class InputBuffer {
    private static final Logger LOGGER = Grizzly.logger(InputBuffer.class);
    private static final Level LOGGER_LEVEL = Level.FINER;
    
    /**
     * The {@link org.glassfish.grizzly.http.HttpHeader} associated with this InputBuffer
     */
    private HttpHeader httpHeader;

    /**
     * The {@link FilterChainContext} associated with this InputBuffer.
     * The {@link FilterChainContext} will be using to read POST
     * data in blocking fashion.
     */
    private FilterChainContext ctx;

    /**
     * Flag indicating whether or not this InputBuffer is processing
     * character data.
     */
    private boolean processingChars;


    /**
     * Flag indicating whether or not this InputBuffer has been
     * closed.
     */
    private boolean closed;

    /**
     * The {@link Buffer} consisting of the bytes from the HTTP
     * message chunk(s).
     */
    private Buffer inputContentBuffer;

    /**
     * The {@link Connection} associated with this {@link org.glassfish.grizzly.http.HttpRequestPacket}.
     */
    private Connection connection;

    /**
     * The mark position within the current binary content.  Marking is not
     * supported for character content.
     */
    private int markPos = -1;

    /**
     * Represents how many bytes can be read before {@link #markPos} is
     * invalidated.
     */
    private int readAheadLimit = -1;

    /**
     * Counter to track how many bytes have been read.  This counter is only
     * used with the byte stream has been marked.
     */
    private int readCount = 0;

    /**
     * The default character encoding to use if none is specified by the
     * {@link org.glassfish.grizzly.http.HttpRequestPacket}.
     */
    private String encoding = DEFAULT_HTTP_CHARACTER_ENCODING;

    /**
     * The {@link CharsetDecoder} used to convert binary to character data.
     */
    private CharsetDecoder decoder;

    /**
     * CharsetDecoders cache
     */
    private final Map decoders = new HashMap<>();

    /**
     * Flag indicating all request content has been read.
     */
    private boolean contentRead;

    /**
     * The {@link ReadHandler} to be notified as content is read.
     */
    private ReadHandler handler;

    /**
     * The length of the content that must be read before notifying the
     * {@link ReadHandler}.
     */
    private int requestedSize;

    /**
     * {@link CharBuffer} for converting a single character at a time.
     */
    private final CharBuffer singleCharBuf =
            (CharBuffer) CharBuffer.allocate(1).position(1); // create CharBuffer w/ 0 chars remaining

    /**
     * Used to estimate how many characters can be produced from a variable
     * number of bytes.
     */
    private float averageCharsPerByte = 1.0f;

    /**
     * Flag shows if we're currently waiting for input data asynchronously
     * (is OP_READ enabled for the Connection)
     */
    private boolean isWaitingDataAsynchronously;

    /**
     * Trailer headers, if any.
     */
    protected Map trailers;


    // ------------------------------------------------------------ Constructors


    /**
     * 

* Per-request initialization required for the InputBuffer to function * properly. *

* * @param httpHeader the header from which input will be obtained. * @param ctx the FilterChainContext for the chain processing this request */ public void initialize(final HttpHeader httpHeader, final FilterChainContext ctx) { if (ctx == null) { throw new IllegalArgumentException("ctx cannot be null."); } this.httpHeader = httpHeader; this.ctx = ctx; connection = ctx.getConnection(); final Object message = ctx.getMessage(); if (message instanceof HttpContent) { final HttpContent content = (HttpContent) message; // Check if HttpContent is chunked message trailer w/ headers checkHttpTrailer(content); updateInputContentBuffer(content.getContent()); contentRead = content.isLast(); if (contentRead) { processTrailers(); } content.recycle(); if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s initialize with ready content: %s", this, inputContentBuffer); } } } /** * Set the default character encoding for this InputBuffer, which * would be applied if no encoding was explicitly set on HTTP * {@link org.glassfish.grizzly.http.HttpRequestPacket} and character decoding * wasn't started yet. */ @SuppressWarnings("UnusedDeclaration") public void setDefaultEncoding(final String encoding) { this.encoding = encoding; } /** *

* Recycle this InputBuffer for reuse. *

*/ public void recycle() { inputContentBuffer.tryDispose(); inputContentBuffer = null; singleCharBuf.position(singleCharBuf.limit()); connection = null; decoder = null; ctx = null; handler = null; trailers = null; processingChars = false; closed = false; contentRead = false; markPos = -1; readAheadLimit = -1; requestedSize = -1; readCount = 0; averageCharsPerByte = 1.0f; isWaitingDataAsynchronously = false; encoding = DEFAULT_HTTP_CHARACTER_ENCODING; } /** *

* This method should be called if the InputBuffer is being used in conjunction * with a {@link java.io.Reader} implementation. If this method is not called, * any character-based methods called on this InputBuffer will * throw a {@link IllegalStateException}. *

*/ public void processingChars() { if (!processingChars) { processingChars = true; final String enc = httpHeader.getCharacterEncoding(); if (enc != null) { encoding = enc; final CharsetDecoder localDecoder = getDecoder(); averageCharsPerByte = localDecoder.averageCharsPerByte(); } } } // --------------------------------------------- InputStream-Related Methods /** * This method always blocks. * @see java.io.InputStream#read() */ public int readByte() throws IOException { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s readByte. Ready content: %s", this, inputContentBuffer); } if (closed) { throw new IOException("Already closed"); } if (!inputContentBuffer.hasRemaining()) { if (fill(1) == -1) { return -1; } } checkMarkAfterRead(1); return inputContentBuffer.get() & 0xFF; } /** * @see java.io.InputStream#read(byte[], int, int) */ public int read(final byte b[], final int off, final int len) throws IOException { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s read byte array of len: %s. Ready content: %s", this, len, inputContentBuffer); } if (closed) { throw new IOException("Already closed"); } if (len == 0) { return 0; } if (!inputContentBuffer.hasRemaining()) { if (fill(1) == -1) { return -1; } } int nlen = Math.min(inputContentBuffer.remaining(), len); inputContentBuffer.get(b, off, nlen); if (!checkMarkAfterRead(nlen)) { inputContentBuffer.shrink(); } return nlen; } /** * Depending on the InputBuffer mode, method will return either * number of available bytes or characters ready to be read without blocking. * * @return depending on the InputBuffer mode, method will return * either number of available bytes or characters ready to be read without * blocking. */ public int readyData() { if (closed) return 0; return ((processingChars) ? availableChar() : available()); } /** * @see java.io.InputStream#available() */ public int available() { return ((closed) ? 0 : inputContentBuffer.remaining()); } /** * Returns the duplicate of the underlying {@link Buffer} used to buffer * incoming request data. The content of the returned buffer will be that * of the underlying buffer. Changes to returned buffer's content will be * visible in the underlying buffer, and vice versa; the two buffers' * position, limit, and mark values will be independent. * * @return the duplicate of the underlying * {@link Buffer} used to buffer incoming request data. */ public Buffer getBuffer() { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s getBuffer. Ready content: %s", this, inputContentBuffer); } return inputContentBuffer.duplicate(); } /** * @return the underlying {@link Buffer} used to buffer incoming request * data. Unlike {@link #getBuffer()}, this method detaches the returned * {@link Buffer}, so user code becomes responsible for handling * the {@link Buffer}. */ public Buffer readBuffer() { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s readBuffer. Ready content: %s", this, inputContentBuffer); } return readBuffer(inputContentBuffer.remaining()); } /** * @param size the requested size of the {@link Buffer} to be returned. * * @return the {@link Buffer} of a given size, which represents a chunk * of the underlying {@link Buffer} which contains incoming request * data. This method detaches the returned * {@link Buffer}, so user code becomes responsible for handling its life-cycle. */ public Buffer readBuffer(final int size) { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s readBuffer(size), size: %s. Ready content: %s", this, size, inputContentBuffer); } final int remaining = inputContentBuffer.remaining(); if (size > remaining) { throw new IllegalStateException("Can not read more bytes than available"); } final Buffer buffer; if (size == remaining) { buffer = inputContentBuffer; inputContentBuffer = Buffers.EMPTY_BUFFER; } else { final Buffer tmpBuffer = inputContentBuffer.split( inputContentBuffer.position() + size); buffer = inputContentBuffer; inputContentBuffer = tmpBuffer; } return buffer; } /** * @return the {@link ReadHandler} current in use, if any. */ public ReadHandler getReadHandler() { return handler; } // -------------------------------------------------- Reader-Related Methods /** * @see java.io.Reader#read(java.nio.CharBuffer) */ public int read(final CharBuffer target) throws IOException { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s read(CharBuffer). Ready content: %s", this, inputContentBuffer); } if (closed) { throw new IOException("Already closed"); } if (!processingChars) { throw new IllegalStateException(); } if (target == null) { throw new IllegalArgumentException("target cannot be null."); } final int read = fillChars(1, target); checkMarkAfterRead(read); return read; } /** * @see java.io.Reader#read() */ public int readChar() throws IOException { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s readChar. Ready content: %s", this, inputContentBuffer); } if (closed) { throw new IOException("Already closed"); } if (!processingChars) { throw new IllegalStateException(); } if (!singleCharBuf.hasRemaining()) { singleCharBuf.clear(); int read = read(singleCharBuf); if (read == -1) { return -1; } } return singleCharBuf.get(); } /** * @see java.io.Reader#read(char[], int, int) */ public int read(final char cbuf[], final int off, final int len) throws IOException { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s read char array, len: %s. Ready content: %s", this, len, inputContentBuffer); } if (closed) { throw new IOException("Already closed"); } if (!processingChars) { throw new IllegalStateException(); } if (len == 0) { return 0; } final CharBuffer buf = CharBuffer.wrap(cbuf, off, len); return read(buf); } /** * @see java.io.Reader#ready() */ public boolean ready() { if (closed) { return false; } if (!processingChars) { throw new IllegalStateException(); } return (inputContentBuffer.hasRemaining() || httpHeader.isExpectContent()); } /** /** * Fill the buffer (blocking) up to the requested length. * * @param length how much content should attempt to buffer, * -1 means buffer entire request. * * @throws IOException if an error occurs reading data from the wire. */ public void fillFully(final int length) throws IOException { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s fillFully, len: %s. Ready content: %s", this, length, inputContentBuffer); } if (length == 0) return; if (length > 0) { final int remaining = length - inputContentBuffer.remaining(); if (remaining > 0) { fill(remaining); } } else { fill(-1); } } public int availableChar() { if (!singleCharBuf.hasRemaining()) { // fill the singleCharBuf to make sure we have at least one char singleCharBuf.clear(); if (fillAvailableChars(1, singleCharBuf) == 0) { singleCharBuf.position(singleCharBuf.limit()); return 0; } singleCharBuf.flip(); } // we have 1 char pre-decoded + estimation for the rest byte[]->char[] count. return 1 + ((int) (inputContentBuffer.remaining() * averageCharsPerByte)); } // ---------------------------------------------------- Common Input Methods /** *

* Supported with binary and character data. *

* * @see java.io.InputStream#mark(int) * @see java.io.Reader#mark(int) */ public void mark(final int readAheadLimit) { if (readAheadLimit > 0) { markPos = inputContentBuffer.position(); readCount = 0; this.readAheadLimit = readAheadLimit; } } /** *

* Only supported with binary data. *

* * @see java.io.InputStream#markSupported() */ public boolean markSupported() { if (processingChars) { throw new IllegalStateException(); } return true; } /** *

* Only supported with binary data. *

* * @see java.io.InputStream#reset() */ public void reset() throws IOException { if (closed) { throw new IOException("Already closed"); } if (readAheadLimit == -1) { throw new IOException("Mark not set"); } readCount = 0; inputContentBuffer.position(markPos); } /** * @see java.io.Closeable#close() */ public void close() throws IOException { closed = true; } /** * Skips the specified number of bytes/characters. * * @see java.io.InputStream#skip(long) * @see java.io.Reader#skip(long) * * @deprecated pls. use {@link #skip(long)}, the block parameter will be ignored */ public long skip(final long n, @SuppressWarnings("UnusedParameters") final boolean block) throws IOException { return skip(n); } /** * Skips the specified number of bytes/characters. * * @see java.io.InputStream#skip(long) * @see java.io.Reader#skip(long) */ public long skip(final long n) throws IOException { if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s skip %s bytes. Ready content: %s", this, n, inputContentBuffer); } if (!processingChars) { if (n <= 0) { return 0L; } if (!inputContentBuffer.hasRemaining()) { if (fill((int) n) == -1) { return -1; } } if (inputContentBuffer.remaining() < n) { fill((int) n); } long nlen = Math.min(inputContentBuffer.remaining(), n); inputContentBuffer.position(inputContentBuffer.position() + (int) nlen); if (!checkMarkAfterRead(n)) { inputContentBuffer.shrink(); } return nlen; } else { if (n < 0) { // required by java.io.Reader.skip() throw new IllegalArgumentException(); } if (n == 0) { return 0L; } final CharBuffer skipBuffer = CharBuffer.allocate((int) n); if (fillChars((int) n, skipBuffer) == -1) { return 0; } return Math.min(skipBuffer.remaining(), n); } } public Map getTrailers() { return trailers; } public boolean areTrailersAvailable() { return trailers != null; } /** * When invoked, this method will call {@link org.glassfish.grizzly.ReadHandler#onAllDataRead()} * on the current {@link ReadHandler} (if any). * * This method shouldn't be invoked by developers directly. */ protected void finished() { if (!contentRead) { contentRead = true; final ReadHandler localHandler = handler; processTrailers(); if (localHandler != null) { handler = null; invokeHandlerAllRead(localHandler, getThreadPool()); } } } private void finishedInTheCurrentThread(final ReadHandler readHandler) { if (!contentRead) { contentRead = true; processTrailers(); if (readHandler != null) { invokeHandlerAllRead(readHandler, null); } } } private void invokeHandlerAllRead(final ReadHandler readHandler, final Executor executor) { if (executor != null) { executor.execute(new Runnable() { @Override public void run() { try { readHandler.onAllDataRead(); } catch (Throwable t) { readHandler.onError(t); } } }); } else { try { readHandler.onAllDataRead(); } catch (Throwable t) { readHandler.onError(t); } } } private void processTrailers() { if (trailers == null) { final MimeHeaders headers = httpHeader.getHeaders(); final int trailerSize = headers.trailerSize(); if (trailerSize > 0) { trailers = new HashMap<>(trailerSize); for (String name : headers.trailerNames()) { trailers.put(name.toLowerCase(), headers.getHeader(name)); } } else { trailers = Collections.emptyMap(); } } } public void replayPayload(final Buffer buffer) { if (!isFinished()) { throw new IllegalStateException("Can't replay when InputBuffer is not closed"); } if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s replayPayload to %s", this, buffer); } closed = false; readCount = 0; readAheadLimit = -1; markPos = -1; inputContentBuffer = buffer; } /** * @return true if all request data has been read, otherwise * returns false. */ public boolean isFinished() { return contentRead; } /** * @return true if this InputBuffer is closed, otherwise * returns false. */ public boolean isClosed() { return closed; } /** * Installs a {@link ReadHandler} that will be notified when any data * becomes available to read without blocking. * * @param handler the {@link ReadHandler} to invoke. * * @throws IllegalArgumentException if handler is null. * @throws IllegalStateException if an attempt is made to register a handler * before an existing registered handler has been invoked or if all request * data has already been read. */ public void notifyAvailable(final ReadHandler handler) { notifyAvailable(handler, 1); } /** * Installs a {@link ReadHandler} that will be notified when the specified * amount of data is available to be read without blocking. * * @param handler the {@link ReadHandler} to invoke. * @param size the minimum number of bytes that must be available before * the {@link ReadHandler} is notified. * * @throws IllegalArgumentException if handler is null, * or if size is less or equal to zero. * @throws IllegalStateException if an attempt is made to register a handler * before an existing registered handler has been invoked. */ public void notifyAvailable(final ReadHandler handler, final int size) { if (handler == null) { throw new IllegalArgumentException("handler cannot be null."); } if (size <= 0) { throw new IllegalArgumentException("size should be positive integer"); } if (this.handler != null) { throw new IllegalStateException("Illegal attempt to register a new handler before the existing handler has been notified"); } // If we don't expect more data - call onAllDataRead() directly if (closed || isFinished()) { try { handler.onAllDataRead(); } catch (Throwable ioe) { handler.onError(ioe); } return; } final int available = readyData(); if (shouldNotifyNow(size, available)) { try { handler.onDataAvailable(); } catch (Throwable ioe) { handler.onError(ioe); } return; } requestedSize = size; this.handler = handler; if (!isWaitingDataAsynchronously) { isWaitingDataAsynchronously = true; initiateAsyncronousDataReceiving(); } } /** * Appends the specified {@link Buffer} to the internal composite * {@link Buffer}. * * @param httpContent the {@link HttpContent} to append * * @return true if {@link ReadHandler} * callback was invoked, otherwise returns false. * * @throws IOException if an error occurs appending the {@link Buffer} */ public boolean append(final HttpContent httpContent) throws IOException { // Stop waiting for data asynchronously and enable it again // only if we have a handler registered, which requirement // (expected size) is not met. isWaitingDataAsynchronously = false; // check if it's broken HTTP content message or not if (!HttpContent.isBroken(httpContent)) { final Buffer buffer = httpContent.getContent(); if (closed) { buffer.dispose(); return false; } final ReadHandler localHandler = handler; final boolean isLast = httpContent.isLast(); // if we have a handler registered - switch the flag to true boolean askForMoreDataInThisThread = !isLast && localHandler != null; boolean invokeDataAvailable = false; if (buffer.hasRemaining()) { updateInputContentBuffer(buffer); if (localHandler != null) { final int available = readyData(); if (available >= requestedSize) { invokeDataAvailable = true; askForMoreDataInThisThread = false; } } } if (askForMoreDataInThisThread) { // There is a ReadHandler registered, but it requested more // data to be available before we can notify it - so wait for // more data to come isWaitingDataAsynchronously = true; return true; } handler = null; if (isLast) { checkHttpTrailer(httpContent); } invokeHandlerOnProperThread(localHandler, invokeDataAvailable, isLast); } else { // broken content final ReadHandler localHandler = handler; handler = null; invokeErrorHandlerOnProperThread(localHandler, ((HttpBrokenContent) httpContent).getException()); } return false; } /** * @return if this buffer is being used to process asynchronous data. * @deprecated will always return true */ public boolean isAsyncEnabled() { // return asyncEnabled; return true; } /** * Sets the asynchronous processing state of this InputBuffer. * * @param asyncEnabled true if this InputBuffer * is to be used to process asynchronous request data. * @deprecated InputBuffer always supports async mode */ @SuppressWarnings("UnusedDeclaration") public void setAsyncEnabled(boolean asyncEnabled) { // this.asyncEnabled = asyncEnabled; } /** *

* Invoke {@link ReadHandler#onError(Throwable)} (assuming a {@link ReadHandler} * is available) } passing a {#link CancellationException} * if the current {@link Connection} is open, or a {#link EOFException} if * the connection was unexpectedly closed. *

* * @since 2.0.1 */ public void terminate() { final ReadHandler localHandler = handler; if (localHandler != null) { handler = null; // call in the current thread, because otherwise handler executed // in the different thread may deal with recycled Request/Response objects localHandler.onError( connection.isOpen() ? new CancellationException() : new EOFException()); } } /** * Initiates asynchronous data receiving. * * This is service method, usually users don't have to call it explicitly. */ public void initiateAsyncronousDataReceiving() { // fork the FilterChainContext execution // keep the current FilterChainContext suspended, but make a copy and resume it ctx.fork(ctx.getStopAction()); } // --------------------------------------------------------- Private Methods /** * @return {@link Executor}, which will be used for notifying user * registered {@link ReadHandler}. */ protected Executor getThreadPool() { if (!Threads.isService()) { return null; } final ExecutorService es = connection.getTransport().getWorkerThreadPool(); return es != null && !es.isShutdown() ? es : null; } private void invokeErrorHandlerOnProperThread(final ReadHandler localHandler, final Throwable error) { if (!closed && localHandler != null) { final Executor executor = getThreadPool(); if (executor != null) { executor.execute(new Runnable() { @Override public void run() { localHandler.onError(error); } }); } else { localHandler.onError(error); } } } private void invokeHandlerOnProperThread(final ReadHandler localHandler, final boolean invokeDataAvailable, final boolean isLast) throws IOException { final Executor executor = getThreadPool(); if (executor != null) { executor.execute(new Runnable() { @Override public void run() { invokeHandler(localHandler, invokeDataAvailable, isLast); } }); } else { invokeHandler(localHandler, invokeDataAvailable, isLast); } } private void invokeHandler(final ReadHandler localHandler, final boolean invokeDataAvailable, final boolean isLast) { try { if (invokeDataAvailable) { localHandler.onDataAvailable(); } if (isLast) { finishedInTheCurrentThread(localHandler); } } catch (Throwable t) { localHandler.onError(t); } } /** * Read next chunk of data in this thread, block if needed. * * @return {@link HttpContent} * @throws IOException if an error occurs reading data from the wire. */ protected HttpContent blockingRead() throws IOException { final ReadResult rr = ctx.read(); final HttpContent c = (HttpContent) rr.getMessage(); rr.recycle(); return c; } /** *

* Used to add additional HTTP message chunk content to {@link #inputContentBuffer}. *

* * @param requestedLen how much content should attempt to be read, * -1 means read till the end of the message. * * @return the number of bytes actually read * * @throws IOException if an I/O error occurs while reading content */ private int fill(final int requestedLen) throws IOException { int read = 0; while ((requestedLen == -1 || read < requestedLen) && httpHeader.isExpectContent()) { final HttpContent c = blockingRead(); final boolean isLast = c.isLast(); // Check if HttpContent is chunked message trailer w/ headers checkHttpTrailer(c); final Buffer b; try { b = c.getContent(); } catch (HttpBrokenContentException e) { final Throwable cause = e.getCause(); throw Exceptions.makeIOException(cause != null ? cause : e); } read += b.remaining(); updateInputContentBuffer(b); c.recycle(); if (isLast) { finished(); break; } } if (read > 0 || requestedLen == 0) { return read; } return -1; } /** *

* Used to convert bytes to chars. *

* * @param requestedLen how much content should attempt to be read * * @return the number of chars actually read * * @throws IOException if an I/O error occurs while reading content */ private int fillChars(final int requestedLen, final CharBuffer dst) throws IOException { int read = 0; // 1) Check pre-decoded singleCharBuf if (dst != singleCharBuf && singleCharBuf.hasRemaining()) { dst.put(singleCharBuf.get()); read = 1; } // 2) Decode available byte[] -> char[] if (inputContentBuffer.hasRemaining()) { read += fillAvailableChars(requestedLen - read, dst); } if (read >= requestedLen) { dst.flip(); return read; } // 3) If we don't expect more data - return what we've read so far if (!httpHeader.isExpectContent()) { dst.flip(); return read > 0 ? read : -1; } // 4) Try to read more data (we may block) CharsetDecoder decoderLocal = getDecoder(); boolean isNeedMoreInput = false; // true, if content in composite buffer is not enough to produce even 1 char boolean last = false; while (read < requestedLen && httpHeader.isExpectContent()) { if (isNeedMoreInput || !inputContentBuffer.hasRemaining()) { final HttpContent c = blockingRead(); updateInputContentBuffer(c.getContent()); last = c.isLast(); c.recycle(); isNeedMoreInput = false; } final ByteBuffer bytes = inputContentBuffer.toByteBuffer(); final int bytesPos = bytes.position(); final int dstPos = dst.position(); final CoderResult result = decoderLocal.decode(bytes, dst, false); final int producedChars = dst.position() - dstPos; final int consumedBytes = bytes.position() - bytesPos; read += producedChars; if (consumedBytes > 0) { bytes.position(bytesPos); inputContentBuffer.position(inputContentBuffer.position() + consumedBytes); if (readAheadLimit == -1) { inputContentBuffer.shrink(); } } else { isNeedMoreInput = true; } if (last || result == CoderResult.OVERFLOW) { break; } } dst.flip(); if (last && read == 0) { read = -1; } return read; } /** *

* Used to convert pre-read (buffered) bytes to chars. *

* * @param requestedLen how much content should attempt to be read * * @return the number of chars actually read */ private int fillAvailableChars(final int requestedLen, final CharBuffer dst) { final CharsetDecoder decoderLocal = getDecoder(); final ByteBuffer bb = inputContentBuffer.toByteBuffer(); final int oldBBPos = bb.position(); int producedChars = 0; int consumedBytes = 0; int producedCharsNow; int consumedBytesNow; CoderResult result; int remaining = requestedLen; do { final int charPos = dst.position(); final int bbPos = bb.position(); result = decoderLocal.decode(bb, dst, false); producedCharsNow = dst.position() - charPos; consumedBytesNow = bb.position() - bbPos; producedChars += producedCharsNow; consumedBytes += consumedBytesNow; remaining -= producedCharsNow; } while (remaining > 0 && (producedCharsNow > 0 || consumedBytesNow > 0) && bb.hasRemaining() && result == CoderResult.UNDERFLOW); bb.position(oldBBPos); inputContentBuffer.position(inputContentBuffer.position() + consumedBytes); if (readAheadLimit == -1) { inputContentBuffer.shrink(); } return producedChars; } protected void updateInputContentBuffer(final Buffer buffer) { buffer.allowBufferDispose(true); if (inputContentBuffer == null) { inputContentBuffer = buffer; } else if (inputContentBuffer.hasRemaining() || readAheadLimit > 0) { // if the stream is marked - we can't dispose the inputContentBuffer, even if it's been read off toCompositeInputContentBuffer().append(buffer); } else { inputContentBuffer.tryDispose(); inputContentBuffer = buffer; } } /** * @param size the amount of data that must be available for a {@link ReadHandler} * to be notified. * @param available the amount of data currently available. * * @return true if the handler should be notified during a call * to {@link #notifyAvailable(ReadHandler)} or {@link #notifyAvailable(ReadHandler, int)}, * otherwise false */ private static boolean shouldNotifyNow(final int size, final int available) { return (available != 0 && available >= size); } /** * @return the {@link CharsetDecoder} that should be used when converting * content from binary to character */ private CharsetDecoder getDecoder() { if (decoder == null) { decoder = decoders.get(encoding); if (decoder == null) { final Charset cs = Charsets.lookupCharset(encoding); decoder = cs.newDecoder(); decoder.onMalformedInput(CodingErrorAction.REPLACE); decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); decoders.put(encoding, decoder); } else { decoder.reset(); } } return decoder; } private CompositeBuffer toCompositeInputContentBuffer() { if (!inputContentBuffer.isComposite()) { final CompositeBuffer compositeBuffer = CompositeBuffer.newBuffer( connection.getMemoryManager()); compositeBuffer.allowBufferDispose(true); compositeBuffer.allowInternalBuffersDispose(true); int posAlign = 0; if (readAheadLimit > 0) { // the simple inputContentBuffer is marked // make the marked data still available inputContentBuffer.position( inputContentBuffer.position() - readCount); posAlign = readCount; markPos = 0; // for the CompositeBuffer markPos is 0 } compositeBuffer.append(inputContentBuffer); compositeBuffer.position(posAlign); inputContentBuffer = compositeBuffer; } return (CompositeBuffer) inputContentBuffer; } /** * @param n read bytes count * @return true if mark is still active, or false if the * mark hasn't been set or has been invalidated */ private boolean checkMarkAfterRead(final long n) { if (n > 0 && readAheadLimit != -1) { if ((long) readCount + n <= readAheadLimit) { readCount += n; return true; } // invalidate the mark readAheadLimit = -1; markPos = -1; readCount = 0; } return false; } /** * Check if passed {@link HttpContent} is {@link HttpTrailer}, which represents * trailer chunk (when chunked Transfer-Encoding is used), if it is a trailer * chunk - then copy all the available trailer headers to request headers map. */ private static void checkHttpTrailer(final HttpContent httpContent) { if (HttpTrailer.isTrailer(httpContent)) { final HttpTrailer httpTrailer = (HttpTrailer) httpContent; final HttpHeader httpHeader = httpContent.getHttpHeader(); httpHeader.getHeaders().mark(); final MimeHeaders trailerHeaders = httpTrailer.getHeaders(); final int size = trailerHeaders.size(); for (int i = 0; i < size; i++) { httpHeader.addHeader( trailerHeaders.getName(i).toString(), trailerHeaders.getValue(i).toString()); } } } private static void log(final String message, Object... params) { final String preparedMsg = String.format(message, params); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, preparedMsg, new Exception("Logged at")); } else { LOGGER.log(LOGGER_LEVEL, preparedMsg); } } }