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

com.pdd.pop.ext.glassfish.grizzly.http.io.OutputBuffer Maven / Gradle / Ivy

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2015 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 com.pdd.pop.ext.glassfish.grizzly.http.io;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.pdd.pop.ext.glassfish.grizzly.Buffer;
import com.pdd.pop.ext.glassfish.grizzly.CompletionHandler;
import com.pdd.pop.ext.glassfish.grizzly.Connection;
import com.pdd.pop.ext.glassfish.grizzly.FileTransfer;
import com.pdd.pop.ext.glassfish.grizzly.Grizzly;
import com.pdd.pop.ext.glassfish.grizzly.IOEvent;
import com.pdd.pop.ext.glassfish.grizzly.WriteHandler;
import com.pdd.pop.ext.glassfish.grizzly.WriteResult;
import com.pdd.pop.ext.glassfish.grizzly.asyncqueue.AsyncQueueWriter;
import com.pdd.pop.ext.glassfish.grizzly.asyncqueue.MessageCloner;
import com.pdd.pop.ext.glassfish.grizzly.filterchain.FilterChainContext;
import com.pdd.pop.ext.glassfish.grizzly.http.HttpContent;
import com.pdd.pop.ext.glassfish.grizzly.http.HttpContext;
import com.pdd.pop.ext.glassfish.grizzly.http.HttpHeader;
import com.pdd.pop.ext.glassfish.grizzly.http.HttpServerFilter;
import com.pdd.pop.ext.glassfish.grizzly.http.util.MimeType;
import com.pdd.pop.ext.glassfish.grizzly.http.util.Header;
import com.pdd.pop.ext.glassfish.grizzly.http.util.HeaderValue;
import com.pdd.pop.ext.glassfish.grizzly.impl.FutureImpl;
import com.pdd.pop.ext.glassfish.grizzly.memory.Buffers;
import com.pdd.pop.ext.glassfish.grizzly.memory.CompositeBuffer;
import com.pdd.pop.ext.glassfish.grizzly.memory.MemoryManager;
import com.pdd.pop.ext.glassfish.grizzly.threadpool.Threads;
import com.pdd.pop.ext.glassfish.grizzly.utils.Charsets;
import com.pdd.pop.ext.glassfish.grizzly.utils.Exceptions;
import com.pdd.pop.ext.glassfish.grizzly.utils.Futures;

import static com.pdd.pop.ext.glassfish.grizzly.Writer.Reentrant;

/**
 * Abstraction exposing both byte and character methods to write content
 * to the HTTP messaging system in Grizzly.
 */
public class OutputBuffer {
    
    protected static final Logger LOGGER = Grizzly.logger(OutputBuffer.class);

    private static final int DEFAULT_BUFFER_SIZE =
            Integer.getInteger(OutputBuffer.class.getName() + ".default-buffer-size",
                    1024 * 8);

    private static final int MAX_CHAR_BUFFER_SIZE =
            Integer.getInteger(OutputBuffer.class.getName() + ".max-char-buffer-size",
                    1024 * 64 + 1);
    
    /**
     * Flag indicating whether or not async operations are being used on the
     * input streams.
     */
    private final static boolean IS_BLOCKING =
            Boolean.getBoolean(OutputBuffer.class.getName() + ".isBlocking");

    private FilterChainContext ctx;

    private CompositeBuffer compositeBuffer;

    private Buffer currentBuffer;

    // Buffer, which is used for write(byte[] ...) scenarios to try to avoid
    // byte arrays copying
    private final TemporaryHeapBuffer temporaryWriteBuffer =
            new TemporaryHeapBuffer();
    // The cloner, which will be responsible for cloning temporaryWriteBuffer,
    // if it's not possible to write its content in this thread
    private final ByteArrayCloner cloner = new ByteArrayCloner(temporaryWriteBuffer);

    private final List lifeCycleListeners =
            new ArrayList(2);

    private boolean committed;

    private boolean finished;

    private boolean closed;

    private CharsetEncoder encoder;

    private final Map encoders =
            new HashMap();

    private char[] charsArray;
    private int charsArrayLength;
    private CharBuffer charsBuffer;

    private MemoryManager memoryManager;
    private Connection connection;
    
    private WriteHandler handler;

    private InternalWriteHandler asyncWriteHandler;

    private boolean fileTransferRequested;

    private int bufferSize = DEFAULT_BUFFER_SIZE;
    
    protected boolean sendfileEnabled;
    
    private HttpHeader outputHeader;

    // Runnable to be used for async notifyWritePossible() notifications,
    // which helps to limit the number of reentrants
    private Runnable writePossibleRunnable;
    
    private HttpContent.Builder builder;
    
    private boolean isNonBlockingWriteGuaranteed;
    private boolean isLastWriteNonBlocking;
    
    private HttpContext httpContext;
    
    
    // ---------------------------------------------------------- Public Methods


    public void initialize(final HttpHeader outputHeader,
                           final boolean sendfileEnabled,
                           final FilterChainContext ctx) {

        this.outputHeader = outputHeader;
        if (builder == null) {
            this.builder = outputHeader.httpContentBuilder();
        } else {
            builder.httpHeader(outputHeader);
        }
        this.sendfileEnabled = sendfileEnabled;
        this.ctx = ctx;
        httpContext = outputHeader.getProcessingState().getHttpContext();
        connection = ctx.getConnection();
        memoryManager = ctx.getMemoryManager();
    }

    /**
     * 

* Returns true if content will be written in a non-blocking * fashion, otherwise returns false. *

* * @return true if content will be written in a non-blocking * fashion, otherwise returns false. * * @since 2.1.2 * @deprecated will always return true */ @SuppressWarnings({"UnusedDeclaration"}) public boolean isAsyncEnabled() { return true; } /** * Sets the asynchronous processing state of this OutputBuffer. * * @param asyncEnabled true if this OutputBuffer * will write content without blocking. * * @since 2.1.2 * @deprecated OutputBuffer always supports async mode */ @SuppressWarnings("UnusedDeclaration") public void setAsyncEnabled(boolean asyncEnabled) { } public void prepareCharacterEncoder() { getEncoder(); } public int getBufferSize() { return bufferSize; } @SuppressWarnings({"UnusedDeclaration"}) public void registerLifeCycleListener(final LifeCycleListener listener) { lifeCycleListeners.add(listener); } @SuppressWarnings({"UnusedDeclaration"}) public boolean removeLifeCycleListener(final LifeCycleListener listener) { return lifeCycleListeners.remove(listener); } public void setBufferSize(final int bufferSize) { if (!committed && currentBuffer == null) { this.bufferSize = bufferSize; } if (charsArray != null && charsArray.length < bufferSize) { final char[] newCharsArray = new char[bufferSize]; System.arraycopy(charsArray, 0, newCharsArray, 0, charsArrayLength); charsBuffer = CharBuffer.wrap(newCharsArray); charsArray = newCharsArray; } } /** * Reset current response. * * @throws IllegalStateException if the response has already been committed */ public void reset() { if (committed) throw new IllegalStateException(/*FIXME:Put an error message*/); compositeBuffer = null; if (currentBuffer != null) { currentBuffer.clear(); } charsArrayLength = 0; encoder = null; } /** * @return true if this OutputBuffer is closed, otherwise * returns false. */ @SuppressWarnings({"UnusedDeclaration"}) public boolean isClosed() { return closed; } /** * Get the number of bytes buffered on OutputBuffer and ready to be sent. * * @return the number of bytes buffered on OutputBuffer and ready to be sent. */ @SuppressWarnings({"UnusedDeclaration"}) public int getBufferedDataSize() { int size = 0; if (compositeBuffer != null) { size += compositeBuffer.remaining(); } if (currentBuffer != null) { size += currentBuffer.position(); } size += (charsArrayLength << 1); return size; } /** * Recycle the output buffer. This should be called when closing the * connection. */ public void recycle() { outputHeader = null; builder.reset(); if (compositeBuffer != null) { compositeBuffer.dispose(); compositeBuffer = null; } if (currentBuffer != null) { currentBuffer.dispose(); currentBuffer = null; } temporaryWriteBuffer.recycle(); if (charsArray != null) { charsArrayLength = 0; if (charsArray.length < MAX_CHAR_BUFFER_SIZE) { charsBuffer.clear(); } else { charsBuffer = null; charsArray = null; } } // charBuf.position(0); bufferSize = DEFAULT_BUFFER_SIZE; fileTransferRequested = false; encoder = null; ctx = null; httpContext = null; connection = null; memoryManager = null; handler = null; isNonBlockingWriteGuaranteed = false; isLastWriteNonBlocking = false; asyncWriteHandler = null; committed = false; finished = false; closed = false; lifeCycleListeners.clear(); } public void endRequest() throws IOException { if (finished) { return; } final InternalWriteHandler asyncWriteQueueHandlerLocal = asyncWriteHandler; if (asyncWriteQueueHandlerLocal != null) { asyncWriteHandler = null; asyncWriteQueueHandlerLocal.detach(); } if (!closed) { try { close(); } catch (IOException ignored) { } } if (ctx != null) { ctx.notifyDownstream(HttpServerFilter.RESPONSE_COMPLETE_EVENT); } finished = true; } /** * Acknowledge a HTTP Expect header. The response status * code and reason phrase should be set before invoking this method. * * @throws IOException if an error occurs writing the acknowledgment. */ public void acknowledge() throws IOException { ctx.write(outputHeader, IS_BLOCKING); } // ---------------------------------------------------- Writer-Based Methods public void writeChar(int c) throws IOException { connection.assertOpen(); if (closed) { return; } updateNonBlockingStatus(); checkCharBuffer(); if (charsArrayLength == charsArray.length) { flushCharsToBuf(true); } charsArray[charsArrayLength++] = (char) c; } public void write(char cbuf[], int off, int len) throws IOException { connection.assertOpen(); if (closed || len == 0) { return; } updateNonBlockingStatus(); checkCharBuffer(); final int remaining = charsArray.length - charsArrayLength; if (len <= remaining) { System.arraycopy(cbuf, off, charsArray, charsArrayLength, len); charsArrayLength += len; } else if (len - remaining < remaining) { System.arraycopy(cbuf, off, charsArray, charsArrayLength, remaining); charsArrayLength += remaining; flushCharsToBuf(true); System.arraycopy(cbuf, off + remaining, charsArray, 0, len - remaining); charsArrayLength = len - remaining; } else { flushCharsToBuf(false); flushCharsToBuf(CharBuffer.wrap(cbuf, off, len), true); } } public void write(final char cbuf[]) throws IOException { write(cbuf, 0, cbuf.length); } public void write(final String str) throws IOException { write(str, 0, str.length()); } public void write(final String str, final int off, final int len) throws IOException { connection.assertOpen(); if (closed || len == 0) { return; } updateNonBlockingStatus(); checkCharBuffer(); if (charsArray.length - charsArrayLength >= len) { str.getChars(off, off + len, charsArray, charsArrayLength); charsArrayLength += len; return; } int offLocal = off; int lenLocal = len; do { final int remaining = charsArray.length - charsArrayLength; final int workingLen = Math.min(lenLocal, remaining); str.getChars(offLocal, offLocal + workingLen, charsArray, charsArrayLength); charsArrayLength += workingLen; offLocal += workingLen; lenLocal -= workingLen; if (lenLocal > 0) { // If string processing is not entirely complete flushCharsToBuf(false); } } while (lenLocal > 0); flushBinaryBuffersIfNeeded(); } // ---------------------------------------------- OutputStream-Based Methods public void writeByte(final int b) throws IOException { connection.assertOpen(); if (closed) { return; } updateNonBlockingStatus(); checkCurrentBuffer(); if (!currentBuffer.hasRemaining()) { if (canWritePayloadChunk()) { doCommit(); flushBinaryBuffers(false); checkCurrentBuffer(); blockAfterWriteIfNeeded(); } else { finishCurrentBuffer(); checkCurrentBuffer(); } } currentBuffer.put((byte) b); } public void write(final byte b[]) throws IOException { write(b, 0, b.length); } /** *

* Calls write(file, 0, file.length()). *

* * @param file the {@link File} to transfer. * @param handler {@link CompletionHandler} that will be notified * of the transfer progress/completion or failure. * * @throws IllegalArgumentException if file is null * * @see #sendfile(java.io.File, long, long, com.pdd.pop.ext.glassfish.grizzly.CompletionHandler) * * @since 2.2 */ public void sendfile(final File file, final CompletionHandler handler) { if (file == null) { throw new IllegalArgumentException("Argument 'file' cannot be null"); } sendfile(file, 0, file.length(), handler); } /** *

* Will use {@link java.nio.channels.FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)} * to send file to the remote endpoint. Note that all headers necessary * for the file transfer must be set prior to invoking this method as this will * case the HTTP header to be flushed to the client prior to sending the file * content. This should also be the last call to write any content to the remote * endpoint. *

* *

* It's required that the response be suspended when using this functionality. * It will be assumed that if the response wasn't suspended when this method * is called, that it's desired that this method manages the suspend/resume cycle. *

* * @param file the {@link File} to transfer. * @param offset the starting offset within the File * @param length the total number of bytes to transfer * @param handler {@link CompletionHandler} that will be notified * of the transfer progress/completion or failure. * * @throws IllegalArgumentException if the response has already been committed * at the time this method was invoked. * @throws IllegalStateException if a file transfer request has already * been made or if send file support isn't * available. * @throws IllegalStateException if the response was in a suspended state * when this method was invoked, but no * {@link CompletionHandler} was provided. * @since 2.2 */ public void sendfile(final File file, final long offset, final long length, final CompletionHandler handler) { if (!sendfileEnabled) { throw new IllegalStateException("sendfile support isn't available."); } if (fileTransferRequested) { throw new IllegalStateException("Only one file transfer allowed per request"); } // clear the internal buffers; sendfile content is exclusive reset(); // additional precondition validation performed by FileTransfer // constructor final FileTransfer f = new FileTransfer(file, offset, length); // lock further sendfile requests out fileTransferRequested = true; outputHeader.setContentLengthLong(f.remaining()); if (outputHeader.getContentType() == null) { outputHeader.setContentType(MimeType.getByFilename(file.getName())); } // set Content-Encoding to identity to prevent compression outputHeader.setHeader(Header.ContentEncoding, HeaderValue.IDENTITY); try { flush(); // commit the headers, then send the file } catch (IOException e) { if (handler != null) { handler.failed(e); } else { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, String.format("Failed to transfer file %s. Cause: %s.", file.getAbsolutePath(), e.getMessage()), e); } } return; } ctx.write(f, handler); } public void write(final byte b[], final int off, final int len) throws IOException { connection.assertOpen(); if (closed || len == 0) { return; } updateNonBlockingStatus(); // Copy the content of the b[] to the currentBuffer, if it's possible if (bufferSize >= len && (currentBuffer == null || currentBuffer.remaining() >= len)) { checkCurrentBuffer(); assert currentBuffer != null; currentBuffer.put(b, off, len); } else if (canWritePayloadChunk()) { // If b[] is too big - try to send it to wire right away (if chunking is allowed) // wrap byte[] with a thread local buffer temporaryWriteBuffer.reset(b, off, len); // if there is data in the currentBuffer - complete it finishCurrentBuffer(); // mark headers as committed doCommit(); if (compositeBuffer != null) { // if we write a composite buffer compositeBuffer.append(temporaryWriteBuffer); flushBuffer(compositeBuffer, false, cloner); compositeBuffer = null; } else { // we write just mutableHeapBuffer content flushBuffer(temporaryWriteBuffer, false, cloner); } blockAfterWriteIfNeeded(); } else { // if we can't write the chunk - buffer it. finishCurrentBuffer(); final Buffer cloneBuffer = memoryManager.allocate(len); cloneBuffer.put(b, off, len); cloneBuffer.flip(); checkCompositeBuffer(); compositeBuffer.append(cloneBuffer); } } // --------------------------------------------------- Common Output Methods public void close() throws IOException { if (closed) { return; } closed = true; connection.assertOpen(); // commit the response (mark it as committed) final boolean isJustCommitted = doCommit(); // Try to commit the content chunk together with headers (if there were not committed before) if (!flushAllBuffers(true) && (isJustCommitted || outputHeader.isChunked())) { // If there is no ready content chunk to commit, // but headers were not committed yet, or this is chunked encoding // and we need to send trailer forceCommitHeaders(true); } blockAfterWriteIfNeeded(); } /** * Flush the response. * * @throws java.io.IOException an underlying I/O error occurred */ public void flush() throws IOException { connection.assertOpen(); final boolean isJustCommitted = doCommit(); if (!flushAllBuffers(false) && isJustCommitted) { forceCommitHeaders(false); } blockAfterWriteIfNeeded(); } /** *

* Writes the contents of the specified {@link ByteBuffer} to the client. *

* * Note, that passed {@link ByteBuffer} will be directly used by underlying * connection, so it could be reused only if it has been flushed. * * @param byteBuffer the {@link ByteBuffer} to write * @throws IOException if an error occurs during the write */ @SuppressWarnings({"unchecked", "UnusedDeclaration"}) public void writeByteBuffer(final ByteBuffer byteBuffer) throws IOException { final Buffer w = Buffers.wrap(memoryManager, byteBuffer); w.allowBufferDispose(false); writeBuffer(w); } /** *

* Writes the contents of the specified {@link Buffer} to the client. *

* * Note, that passed {@link Buffer} will be directly used by underlying * connection, so it could be reused only if it has been flushed. * * @param buffer the {@link Buffer} to write * @throws IOException if an error occurs during the write */ public void writeBuffer(final Buffer buffer) throws IOException { connection.assertOpen(); updateNonBlockingStatus(); finishCurrentBuffer(); checkCompositeBuffer(); compositeBuffer.append(buffer); if (canWritePayloadChunk() && compositeBuffer.remaining() > bufferSize) { flush(); } } // -------------------------------------------------- General Public Methods @SuppressWarnings("UnusedDeclaration") @Deprecated public boolean canWriteChar(final int length) { return canWrite(); } /** * @see AsyncQueueWriter#canWrite(com.pdd.pop.ext.glassfish.grizzly.Connection, int) * @deprecated the length parameter will be ignored. Please use {@link #canWrite()}. */ @SuppressWarnings("UnusedParameters") public boolean canWrite(@SuppressWarnings("UnusedParameters") final int length) { return canWrite(); } /** * @see AsyncQueueWriter#canWrite(com.pdd.pop.ext.glassfish.grizzly.Connection) */ public boolean canWrite() { if (IS_BLOCKING || isNonBlockingWriteGuaranteed) { return true; } if (httpContext.getOutputSink().canWrite()) { isNonBlockingWriteGuaranteed = true; return true; } return false; } /** * @see AsyncQueueWriter#notifyWritePossible(com.pdd.pop.ext.glassfish.grizzly.Connection, com.pdd.pop.ext.glassfish.grizzly.WriteHandler, int) * @deprecated the length parameter will be ignored. Please use {@link #notifyCanWrite(com.pdd.pop.ext.glassfish.grizzly.WriteHandler)}. */ @SuppressWarnings("UnusedParameters") public void notifyCanWrite(final WriteHandler handler, @SuppressWarnings("UnusedParameters") final int length) { notifyCanWrite(handler); } public void notifyCanWrite(final WriteHandler handler) { if (this.handler != null) { throw new IllegalStateException("Illegal attempt to set a new handler before the existing handler has been notified."); } if (!httpContext.getCloseable().isOpen()) { handler.onError(connection.getCloseReason().getCause()); return; } this.handler = handler; if (isNonBlockingWriteGuaranteed || canWrite()) { final Reentrant reentrant = Reentrant.getWriteReentrant(); if (!reentrant.isMaxReentrantsReached()) { notifyWritePossible(); } else { notifyWritePossibleAsync(); } return; } // This point might be reached if OutputBuffer is in non-blocking mode assert !IS_BLOCKING; if (asyncWriteHandler == null) { asyncWriteHandler = new InternalWriteHandler(this); } try { // If exception occurs here - it's from WriteHandler, so it must // have been processed by WriteHandler.onError(). httpContext.getOutputSink().notifyCanWrite(asyncWriteHandler); } catch (Exception ignored) { } } /** * @return {@link Executor}, which will be used for notifying user * registered {@link WriteHandler}. */ protected Executor getThreadPool() { if (!Threads.isService()) { return null; } final ExecutorService es = connection.getTransport().getWorkerThreadPool(); return es != null && !es.isShutdown() ? es : null; } /** * Notify WriteHandler asynchronously */ @SuppressWarnings("unchecked") private void notifyWritePossibleAsync() { if (writePossibleRunnable == null) { writePossibleRunnable = new Runnable() { @Override public void run() { notifyWritePossible(); } }; } connection.executeInEventThread(IOEvent.WRITE, writePossibleRunnable); } /** * Notify WriteHandler */ private void notifyWritePossible() { final WriteHandler localHandler = handler; if (localHandler != null) { final Reentrant reentrant = Reentrant.getWriteReentrant(); try { handler = null; reentrant.inc(); isNonBlockingWriteGuaranteed = true; localHandler.onWritePossible(); } catch (Throwable t) { localHandler.onError(t); } finally { reentrant.dec(); } } } // --------------------------------------------------------- Private Methods private boolean canWritePayloadChunk() { return outputHeader.isChunkingAllowed() || outputHeader.getContentLength() != -1; } private void blockAfterWriteIfNeeded() throws IOException { if (IS_BLOCKING || isNonBlockingWriteGuaranteed || isLastWriteNonBlocking) { return; } if (httpContext.getOutputSink().canWrite()) { return; } final FutureImpl future = Futures.createSafeFuture(); httpContext.getOutputSink().notifyCanWrite(new WriteHandler() { @Override public void onWritePossible() throws Exception { future.result(Boolean.TRUE); } @Override public void onError(Throwable t) { future.failure(Exceptions.makeIOException(t)); } }); try { final long writeTimeout = connection.getWriteTimeout(TimeUnit.MILLISECONDS); if (writeTimeout >= 0) { future.get(writeTimeout, TimeUnit.MILLISECONDS); } else { future.get(); } } catch (ExecutionException e) { // close http I/O so that the response will not be broken httpContext.close(); throw Exceptions.makeIOException(e.getCause()); } catch (TimeoutException e) { // close http I/O so that the response will not be broken httpContext.close(); throw new IOException("Write timeout exceeded when trying to flush the data"); } catch (Exception e) { // close http I/O so that the response will not be broken httpContext.close(); throw Exceptions.makeIOException(e); } } private boolean flushAllBuffers(final boolean isLast) throws IOException { if (charsArrayLength > 0) { flushCharsToBuf(false); } return flushBinaryBuffers(isLast); } private boolean flushBinaryBuffers(final boolean isLast) throws IOException { if (!outputHeader.isChunkingAllowed() && outputHeader.getContentLength() == -1) { if (!isLast) { return false; } else { outputHeader.setContentLength(getBufferedDataSize()); } } final Buffer bufferToFlush; final boolean isFlushComposite = compositeBuffer != null && compositeBuffer.hasRemaining(); if (isFlushComposite) { finishCurrentBuffer(); bufferToFlush = compositeBuffer; compositeBuffer = null; } else if (currentBuffer != null && currentBuffer.position() > 0) { currentBuffer.trim(); bufferToFlush = currentBuffer; currentBuffer = null; } else { bufferToFlush = null; } if (bufferToFlush != null) { flushBuffer(bufferToFlush, isLast, null); return true; } return false; } private void flushBuffer(final Buffer bufferToFlush, final boolean isLast, final MessageCloner messageCloner) throws IOException { builder.content(bufferToFlush).last(isLast); ctx.write(null, builder.build(), null, messageCloner, IS_BLOCKING); } private void checkCharBuffer() { if (charsArray == null) { charsArray = new char[bufferSize]; charsBuffer = CharBuffer.wrap(charsArray); } } private void checkCurrentBuffer() { if (currentBuffer == null) { currentBuffer = memoryManager.allocate(bufferSize); currentBuffer.allowBufferDispose(true); } } private void finishCurrentBuffer() { if (currentBuffer != null && currentBuffer.position() > 0) { currentBuffer.trim(); checkCompositeBuffer(); compositeBuffer.append(currentBuffer); currentBuffer = null; } } private CharsetEncoder getEncoder() { if (encoder == null) { String encoding = outputHeader.getCharacterEncoding(); if (encoding == null) { encoding = com.pdd.pop.ext.glassfish.grizzly.http.util.Constants.DEFAULT_HTTP_CHARACTER_ENCODING; } encoder = encoders.get(encoding); if (encoder == null) { final Charset cs = Charsets.lookupCharset(encoding); encoder = cs.newEncoder(); encoder.onMalformedInput(CodingErrorAction.REPLACE); encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); encoders.put(encoding, encoder); } else { encoder.reset(); } } return encoder; } private boolean doCommit() throws IOException { if (!committed) { notifyCommit(); committed = true; return true; } return false; } private void forceCommitHeaders(final boolean isLast) throws IOException { if (isLast) { if (outputHeader != null) { builder.last(true).content(null); ctx.write(builder.build(), IS_BLOCKING); } } else { ctx.write(outputHeader, IS_BLOCKING); } } private void checkCompositeBuffer() { if (compositeBuffer == null) { final CompositeBuffer buffer = CompositeBuffer.newBuffer( memoryManager); buffer.allowBufferDispose(true); buffer.allowInternalBuffersDispose(true); compositeBuffer = buffer; } } private void flushCharsToBuf(final boolean canFlushToNet) throws IOException { charsBuffer.limit(charsArrayLength); try { flushCharsToBuf(charsBuffer, canFlushToNet); } finally { charsArrayLength = 0; charsBuffer.clear(); } } private void flushCharsToBuf(final CharBuffer charBuf, final boolean canFlushToNet) throws IOException { if (!charBuf.hasRemaining()) return; // flush the buffer - need to take care of encoding at this point final CharsetEncoder enc = getEncoder(); checkCurrentBuffer(); if (!currentBuffer.hasRemaining()) { finishCurrentBuffer(); checkCurrentBuffer(); } ByteBuffer currentByteBuffer = currentBuffer.toByteBuffer(); int bufferPos = currentBuffer.position(); int byteBufferPos = currentByteBuffer.position(); CoderResult res = enc.encode(charBuf, currentByteBuffer, true); currentBuffer.position(bufferPos + (currentByteBuffer.position() - byteBufferPos)); while (res == CoderResult.OVERFLOW) { checkCurrentBuffer(); currentByteBuffer = currentBuffer.toByteBuffer(); bufferPos = currentBuffer.position(); byteBufferPos = currentByteBuffer.position(); res = enc.encode(charBuf, currentByteBuffer, true); currentBuffer.position(bufferPos + (currentByteBuffer.position() - byteBufferPos)); if (res == CoderResult.OVERFLOW) { finishCurrentBuffer(); } } if (res != CoderResult.UNDERFLOW) { throw new IOException("Encoding error"); } if (canFlushToNet) { // this actually checks wheather current buffer was overloaded during encoding so we need to flush flushBinaryBuffersIfNeeded(); } } private void flushBinaryBuffersIfNeeded() throws IOException { if (compositeBuffer != null) { // this actually checks wheather current buffer was overloaded during encoding so we need to flush doCommit(); flushBinaryBuffers(false); blockAfterWriteIfNeeded(); } } private void notifyCommit() throws IOException { for (int i = 0, len = lifeCycleListeners.size(); i < len; i++) { lifeCycleListeners.get(i).onCommit(); } } private void updateNonBlockingStatus() { isLastWriteNonBlocking = isNonBlockingWriteGuaranteed; isNonBlockingWriteGuaranteed = false; } /** * The {@link MessageCloner}, responsible for cloning Buffer content, if it * wasn't possible to write it in the current Thread (it was added to async * write queue). * We do this, because {@link #write(byte[], int, int)} method is not aware * of async write queues, and content of the passed byte[] might be changed * by user application once in gets control back. */ static final class ByteArrayCloner implements MessageCloner { private final TemporaryHeapBuffer temporaryWriteBuffer; public ByteArrayCloner(final TemporaryHeapBuffer temporaryWriteBuffer) { this.temporaryWriteBuffer = temporaryWriteBuffer; } @Override public Buffer clone(final Connection connection, final Buffer originalMessage) { // Buffer was disposed somewhere on the way to async write queue - // just return the original message if (temporaryWriteBuffer.isDisposed()) { return originalMessage; } return clone0(connection.getMemoryManager(), originalMessage); } Buffer clone0(final MemoryManager memoryManager, final Buffer originalMessage) { if (originalMessage.isComposite()) { final CompositeBuffer compositeBuffer = (CompositeBuffer) originalMessage; compositeBuffer.shrink(); if (!temporaryWriteBuffer.isDisposed()) { if (compositeBuffer.remaining() == temporaryWriteBuffer.remaining()) { compositeBuffer.allowInternalBuffersDispose(false); compositeBuffer.tryDispose(); return temporaryWriteBuffer.cloneContent(memoryManager); } else { compositeBuffer.replace(temporaryWriteBuffer, temporaryWriteBuffer.cloneContent(memoryManager)); } } return originalMessage; } return temporaryWriteBuffer.cloneContent(memoryManager); } } public interface LifeCycleListener { void onCommit() throws IOException; } private static class InternalWriteHandler implements WriteHandler { private volatile OutputBuffer outputBuffer; public InternalWriteHandler(final OutputBuffer outputBuffer) { this.outputBuffer = outputBuffer; } public void detach() { final OutputBuffer obLocal = outputBuffer; if (obLocal != null) { outputBuffer = null; // call in the current thread, because otherwise handler executed // in the different thread may deal with recycled Request/Response objects onError0(obLocal, obLocal.httpContext.getCloseable().isOpen() ? new CancellationException() : new EOFException()); } } @Override public void onWritePossible() throws Exception { final OutputBuffer localOB = outputBuffer; if (localOB != null) { final Executor executor = localOB.getThreadPool(); if (executor != null) { executor.execute(new Runnable() { @Override public void run() { try { onWritePossible0(localOB); } catch (Exception ignored) { // exceptions ignored by implementation - safe to // ignore here as well. } } }); } else { onWritePossible0(localOB); } } } @Override public void onError(final Throwable t) { final OutputBuffer localOB = outputBuffer; if (localOB != null) { final Executor executor = localOB.getThreadPool(); if (executor != null) { executor.execute(new Runnable() { @Override public void run() { onError0(localOB, t); } }); } else { onError0(localOB, t); } } } // ----------------------------------------------------- Private Methods private static void onWritePossible0(final OutputBuffer ob) throws Exception { try { final Reentrant reentrant = Reentrant.getWriteReentrant(); if (!reentrant.isMaxReentrantsReached()) { ob.notifyWritePossible(); } else { ob.notifyWritePossibleAsync(); } } catch (Exception ignored) { } } private static void onError0(final OutputBuffer ob, final Throwable t) { final WriteHandler localHandler = ob.handler; if (localHandler != null) { try { ob.handler = null; localHandler.onError(t); } catch (Exception ignored) { } } } } }