org.eclipse.jetty.ee8.nested.HttpOutput Maven / Gradle / Ivy
Show all versions of jetty-ee8-nested Show documentation
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee8.nested;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritePendingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.concurrent.CancellationException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.WriteListener;
import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.SharedBlockingCallback;
import org.eclipse.jetty.util.SharedBlockingCallback.Blocker;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.ThreadIdPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link HttpOutput} implements {@link ServletOutputStream}
* as required by the Servlet specification.
* {@link HttpOutput} buffers content written by the application until a
* further write will overflow the buffer, at which point it triggers a commit
* of the response.
* {@link HttpOutput} can be closed and reopened, to allow requests included
* via {@link RequestDispatcher#include(ServletRequest, ServletResponse)} to
* close the stream, to be reopened after the inclusion ends.
*/
public class HttpOutput extends ServletOutputStream implements Runnable {
/**
* The output state
*/
enum State {
// Open
OPEN,
// Close needed from onWriteComplete
CLOSE,
// Close in progress after close API called
CLOSING,
// Closed
CLOSED
}
/**
* The API State which combines with the output State:
*
* OPEN/BLOCKING---last----------------------------+ CLOSED/BLOCKING
* / | ^ \ ^ ^
* / w | \ / |
* / | owc +--owcL------------------->--owcL-----\---------------------+ |
* | v | / / V |
* swl OPEN/BLOCKED----last---->CLOSE/BLOCKED----owc----->CLOSING/BLOCKED--owcL------+
* |
* \
* \
* V
* +-->OPEN/READY------last---------------------------+
* / ^ | \
* / / w \
* | / | +--owcL------------------->--owcL----\---------------------------+
* | / v / / V |
* | irt OPEN/PENDING----last---->CLOSE/PENDING----owc---->CLOSING/PENDING--owcL----+ |
* | \ / | | ^ | | |
* owc \/ owc irf / irf | |
* | /\ | | / | | |
* | / \ V | / | V V
* | irf OPEN/ASYNC------last----------|----------------+ | CLOSED/ASYNC
* | \ | | ^ ^
* \ \ | | | |
* \ \ | | | |
* \ v v v | |
* +--OPEN/UNREADY----last---->CLOSE/UNREADY----owc----->CLOSING/UNREADY--owcL---+ |
* \ \ |
* +--owcL------------------->--owcL--------------------------------+
*
* swl : setWriteListener
* w : write
* owc : onWriteComplete last == false
* owcL : onWriteComplete last == true
* irf : isReady() == false
* irt : isReady() == true
* last : close() or complete(Callback) or write of known last content
*
*/
enum ApiState {
// Open in blocking mode
BLOCKING,
// Blocked in blocking operation
BLOCKED,
// Open in async mode
ASYNC,
// isReady() has returned true
READY,
// write operating in progress
PENDING,
// write operating in progress, isReady has returned false
UNREADY
}
/**
* The HttpOutput.Interceptor is a single intercept point for all
* output written to the HttpOutput: via writer; via output stream;
* asynchronously; or blocking.
*
* The Interceptor can be used to implement translations (eg Gzip) or
* additional buffering that acts on all output. Interceptors are
* created in a chain, so that multiple concerns may intercept.
*
* The {@link HttpChannel} is an {@link Interceptor} and is always the
* last link in any Interceptor chain.
*
* Responses are committed by the first call to
* {@link #write(ByteBuffer, boolean, Callback)}
* and closed by a call to {@link #write(ByteBuffer, boolean, Callback)}
* with the last boolean set true. If no content is available to commit
* or close, then a null buffer is passed.
*/
public interface Interceptor extends Content.Sink {
/**
* Write content.
* The response is committed by the first call to write and is closed by
* a call with last == true. Empty content buffers may be passed to
* force a commit or close.
*
* @param content The content to be written or an empty buffer.
* @param last True if this is the last call to write
* @param callback The callback to use to indicate {@link Callback#succeeded()}
* or {@link Callback#failed(Throwable)}.
*/
void write(ByteBuffer content, boolean last, Callback callback);
@Override
default void write(boolean last, ByteBuffer content, Callback callback) {
write(content, last, callback);
}
/**
* @return The next Interceptor in the chain or null if this is the
* last Interceptor in the chain.
*/
Interceptor getNextInterceptor();
/**
* Reset the buffers.
*
If the Interceptor contains buffers then reset them.
*
* @throws IllegalStateException Thrown if the response has been
* committed and buffers and/or headers cannot be reset.
*/
default void resetBuffer() throws IllegalStateException {
Interceptor next = getNextInterceptor();
if (next != null)
next.resetBuffer();
}
}
private static final Logger LOG = LoggerFactory.getLogger(HttpOutput.class);
private static final ThreadIdPool _encoder = new ThreadIdPool<>();
private final HttpChannel _channel;
private final HttpChannelState _channelState;
private final SharedBlockingCallback _writeBlocker;
private ApiState _apiState = ApiState.BLOCKING;
private State _state = State.OPEN;
private boolean _softClose = false;
private Interceptor _interceptor;
private long _written;
private long _firstByteNanoTime = -1;
private ByteBufferPool.Sized _pool;
private RetainableByteBuffer _aggregate;
private int _bufferSize;
private int _commitSize;
private WriteListener _writeListener;
private volatile Throwable _onError;
private Callback _closedCallback;
public HttpOutput(HttpChannel channel) {
_channel = channel;
_channelState = channel.getState();
_interceptor = channel;
_writeBlocker = new WriteBlocker(channel);
HttpConfiguration config = channel.getHttpConfiguration();
_bufferSize = config.getOutputBufferSize();
_commitSize = config.getOutputAggregationSize();
if (_commitSize > _bufferSize) {
LOG.warn("OutputAggregationSize {} exceeds bufferSize {}", _commitSize, _bufferSize);
_commitSize = _bufferSize;
}
}
public HttpChannel getHttpChannel() {
return _channel;
}
public Interceptor getInterceptor() {
return _interceptor;
}
public void setInterceptor(Interceptor interceptor) {
_interceptor = interceptor;
}
public boolean isWritten() {
return _written > 0;
}
public long getWritten() {
return _written;
}
public void reopen() {
try (AutoLock l = _channelState.lock()) {
_softClose = false;
}
}
protected Blocker acquireWriteBlockingCallback() throws IOException {
return _writeBlocker.acquire();
}
private void channelWrite(ByteBuffer content, boolean complete) throws IOException {
try (Blocker blocker = _writeBlocker.acquire()) {
channelWrite(content, complete, blocker);
blocker.block();
}
}
private void channelWrite(ByteBuffer content, boolean last, Callback callback) {
if (_firstByteNanoTime == -1) {
long minDataRate = getHttpChannel().getHttpConfiguration().getMinResponseDataRate();
if (minDataRate > 0)
_firstByteNanoTime = NanoTime.now();
else
_firstByteNanoTime = Long.MAX_VALUE;
}
_interceptor.write(content, last, callback);
}
private void onWriteComplete(boolean last, Throwable failure) {
String state = null;
boolean wake = false;
Callback closedCallback = null;
ByteBuffer closeContent = null;
try (AutoLock l = _channelState.lock()) {
if (LOG.isDebugEnabled())
state = stateString();
// Transition to CLOSED state if we were the last write or we have failed
if (last || failure != null) {
_state = State.CLOSED;
closedCallback = _closedCallback;
_closedCallback = null;
if (failure == null)
lockedReleaseBuffer();
wake = updateApiState(failure);
} else if (_state == State.CLOSE) {
// Somebody called close or complete while we were writing.
// We can now send a (probably empty) last buffer and then when it completes
// onWriteComplete will be called again to actually execute the _completeCallback
_state = State.CLOSING;
closeContent = _aggregate != null && _aggregate.hasRemaining() ? _aggregate.getByteBuffer() : BufferUtil.EMPTY_BUFFER;
} else {
wake = updateApiState(null);
}
}
if (LOG.isDebugEnabled())
LOG.debug("onWriteComplete({},{}) {}->{} c={} cb={} w={}", last, failure, state, stateString(), BufferUtil.toDetailString(closeContent), closedCallback, wake, failure);
try {
if (closedCallback != null) {
if (failure == null)
closedCallback.succeeded();
else
closedCallback.failed(failure);
} else if (closeContent != null) {
channelWrite(closeContent, true, new WriteCompleteCB());
}
} finally {
if (wake)
// TODO review in jetty-10 if execute is needed
_channel.execute(_channel);
}
}
private boolean updateApiState(Throwable failure) {
boolean wake = false;
switch(_apiState) {
case BLOCKED:
_apiState = ApiState.BLOCKING;
break;
case PENDING:
_apiState = ApiState.ASYNC;
if (failure != null) {
_onError = failure;
wake = _channelState.onWritePossible();
}
break;
case UNREADY:
_apiState = ApiState.READY;
if (failure != null)
_onError = failure;
wake = _channelState.onWritePossible();
break;
default:
if (_state == State.CLOSED)
break;
throw new IllegalStateException(stateString());
}
return wake;
}
private int maximizeAggregateSpace() {
// If no aggregate, we can allocate one of bufferSize
if (_aggregate == null)
return getBufferSize();
ByteBuffer byteBuffer = _aggregate.getByteBuffer();
// compact to maximize space
BufferUtil.compact(byteBuffer);
return BufferUtil.space(byteBuffer);
}
public void softClose() {
try (AutoLock l = _channelState.lock()) {
_softClose = true;
}
}
public void complete(Callback callback) {
// This method is invoked for the COMPLETE action handling in
// HttpChannel.handle. The callback passed typically will call completed
// to finish the request cycle and so may need to asynchronously wait for:
// a pending/blocked operation to finish and then either an async close or
// wait for an application close to complete.
boolean succeeded = false;
Throwable error = null;
ByteBuffer content = null;
try (AutoLock l = _channelState.lock()) {
// First check the API state for any unrecoverable situations
switch(_apiState) {
case // isReady() has returned false so a call to onWritePossible may happen at any time
UNREADY:
error = new CancellationException("Completed whilst write unready");
break;
case // an async write is pending and may complete at any time
PENDING:
// If this is not the last write, then we must abort
if (!_channel.getResponse().isContentComplete(_written))
error = new CancellationException("Completed whilst write pending");
break;
case // another thread is blocked in a write or a close
BLOCKED:
error = new CancellationException("Completed whilst write blocked");
break;
default:
break;
}
// If we can't complete due to the API state, then abort
if (error != null) {
_channel.abort(error);
_writeBlocker.fail(error);
_state = State.CLOSED;
} else {
// Otherwise check the output state to determine how to complete
switch(_state) {
case CLOSED:
succeeded = true;
break;
case CLOSE:
case CLOSING:
_closedCallback = Callback.combine(_closedCallback, callback);
break;
case OPEN:
if (_onError != null) {
error = _onError;
break;
}
_closedCallback = Callback.combine(_closedCallback, callback);
switch(_apiState) {
case BLOCKING:
// Output is idle blocking state, but we still do an async close
_apiState = ApiState.BLOCKED;
_state = State.CLOSING;
content = _aggregate != null && _aggregate.hasRemaining() ? _aggregate.getByteBuffer() : BufferUtil.EMPTY_BUFFER;
break;
case ASYNC:
case READY:
// Output is idle in async state, so we can do an async close
_apiState = ApiState.PENDING;
_state = State.CLOSING;
content = _aggregate != null && _aggregate.hasRemaining() ? _aggregate.getByteBuffer() : BufferUtil.EMPTY_BUFFER;
break;
case UNREADY:
case PENDING:
// An operation is in progress, so we soft close now
_softClose = true;
// then trigger a close from onWriteComplete
_state = State.CLOSE;
break;
default:
throw new IllegalStateException();
}
break;
}
}
}
if (LOG.isDebugEnabled())
LOG.debug("complete({}) {} s={} e={}, c={}", callback, stateString(), succeeded, error, BufferUtil.toDetailString(content));
if (succeeded) {
callback.succeeded();
return;
}
if (error != null) {
callback.failed(error);
return;
}
if (content != null)
channelWrite(content, true, new WriteCompleteCB());
}
/**
* Called to indicate that the request cycle has been completed.
*/
public void completed(Throwable failure) {
try (AutoLock l = _channelState.lock()) {
_state = State.CLOSED;
lockedReleaseBuffer();
}
}
@Override
public void close() throws IOException {
ByteBuffer content = null;
Blocker blocker = null;
try (AutoLock l = _channelState.lock()) {
if (_onError != null) {
if (_onError instanceof IOException)
throw (IOException) _onError;
if (_onError instanceof RuntimeException)
throw (RuntimeException) _onError;
if (_onError instanceof Error)
throw (Error) _onError;
throw new IOException(_onError);
}
switch(_state) {
case CLOSED:
break;
case CLOSE:
case CLOSING:
switch(_apiState) {
case BLOCKING:
case BLOCKED:
// block until CLOSED state reached.
blocker = _writeBlocker.acquire();
_closedCallback = Callback.combine(_closedCallback, blocker);
break;
default:
// async close with no callback, so nothing to do
break;
}
break;
case OPEN:
switch(_apiState) {
case BLOCKING:
// Output is idle blocking state, but we still do an async close
_apiState = ApiState.BLOCKED;
_state = State.CLOSING;
blocker = _writeBlocker.acquire();
content = _aggregate != null && _aggregate.hasRemaining() ? _aggregate.getByteBuffer() : BufferUtil.EMPTY_BUFFER;
break;
case BLOCKED:
// An blocking operation is in progress, so we soft close now
_softClose = true;
// then trigger a close from onWriteComplete
_state = State.CLOSE;
// and block until it is complete
blocker = _writeBlocker.acquire();
_closedCallback = Callback.combine(_closedCallback, blocker);
break;
case ASYNC:
case READY:
// Output is idle in async state, so we can do an async close
_apiState = ApiState.PENDING;
_state = State.CLOSING;
content = _aggregate != null && _aggregate.hasRemaining() ? _aggregate.getByteBuffer() : BufferUtil.EMPTY_BUFFER;
break;
case UNREADY:
case PENDING:
// An async operation is in progress, so we soft close now
_softClose = true;
// then trigger a close from onWriteComplete
_state = State.CLOSE;
break;
}
break;
}
}
if (LOG.isDebugEnabled())
LOG.debug("close() {} c={} b={}", stateString(), BufferUtil.toDetailString(content), blocker);
if (content == null) {
if (blocker == null)
// nothing to do or block for.
return;
// Just wait for some other close to finish.
try (Blocker b = blocker) {
b.block();
}
} else {
if (blocker == null) {
// Do an async close
channelWrite(content, true, new WriteCompleteCB());
} else {
// Do a blocking close
try (Blocker b = blocker) {
channelWrite(content, true, blocker);
b.block();
onWriteComplete(true, null);
} catch (Throwable t) {
onWriteComplete(true, t);
throw t;
}
}
}
}
public ByteBuffer getByteBuffer() {
try (AutoLock l = _channelState.lock()) {
return lockedAcquireBuffer().getByteBuffer();
}
}
private ByteBufferPool.Sized getSizedByteBufferPool() {
int bufferSize = getBufferSize();
boolean useOutputDirectByteBuffers = _channel.isUseOutputDirectByteBuffers();
if (_pool == null || _pool.getSize() != bufferSize || _pool.isDirect() != useOutputDirectByteBuffers)
_pool = new ByteBufferPool.Sized(_channel.getByteBufferPool(), useOutputDirectByteBuffers, bufferSize);
return _pool;
}
private RetainableByteBuffer lockedAcquireBuffer() {
assert _channelState.isLockHeldByCurrentThread();
if (_aggregate == null)
_aggregate = getSizedByteBufferPool().acquire();
return _aggregate;
}
private void lockedReleaseBuffer() {
assert _channelState.isLockHeldByCurrentThread();
if (_aggregate != null) {
_aggregate.release();
_aggregate = null;
_pool = null;
}
}
public boolean isClosed() {
try (AutoLock l = _channelState.lock()) {
return _softClose || (_state != State.OPEN);
}
}
public boolean isAsync() {
try (AutoLock l = _channelState.lock()) {
switch(_apiState) {
case ASYNC:
case READY:
case PENDING:
case UNREADY:
return true;
default:
return false;
}
}
}
@Override
public void flush() throws IOException {
ByteBuffer content = null;
try (AutoLock l = _channelState.lock()) {
switch(_state) {
case CLOSED:
case CLOSING:
return;
default:
{
switch(_apiState) {
case BLOCKING:
_apiState = ApiState.BLOCKED;
content = _aggregate != null && _aggregate.hasRemaining() ? _aggregate.getByteBuffer() : BufferUtil.EMPTY_BUFFER;
break;
case ASYNC:
case PENDING:
throw new IllegalStateException("isReady() not called: " + stateString());
case READY:
_apiState = ApiState.PENDING;
break;
case UNREADY:
throw new WritePendingException();
default:
throw new IllegalStateException(stateString());
}
}
}
}
if (content == null) {
new AsyncFlush(false).iterate();
} else {
try {
channelWrite(content, false);
onWriteComplete(false, null);
} catch (Throwable t) {
onWriteComplete(false, t);
if (t instanceof IOException)
throw t;
throw new IOException(t);
}
}
}
private void checkWritable() throws EofException {
if (_softClose)
throw new EofException("Closed");
switch(_state) {
case CLOSED:
case CLOSING:
throw new EofException("Closed");
default:
break;
}
if (_onError != null)
throw new EofException(_onError);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (LOG.isDebugEnabled())
LOG.debug("write(array {})", BufferUtil.toDetailString(ByteBuffer.wrap(b, off, len)));
boolean last;
boolean aggregate;
boolean flush;
// Async or Blocking ?
boolean async;
try (AutoLock l = _channelState.lock()) {
checkWritable();
long written = _written + len;
int space = maximizeAggregateSpace();
last = _channel.getResponse().isAllContentWritten(written);
// Write will be aggregated if:
// + it is smaller than the commitSize
// + is not the last one, or is last but will fit in an already allocated aggregate buffer.
aggregate = len <= _commitSize && (!last || (_aggregate != null && _aggregate.hasRemaining()) && len <= space);
flush = last || !aggregate || len >= space;
if (last && _state == State.OPEN)
_state = State.CLOSING;
switch(_apiState) {
case BLOCKING:
_apiState = flush ? ApiState.BLOCKED : ApiState.BLOCKING;
async = false;
break;
case ASYNC:
throw new IllegalStateException("isReady() not called: " + stateString());
case READY:
async = true;
_apiState = flush ? ApiState.PENDING : ApiState.ASYNC;
break;
case PENDING:
case UNREADY:
throw new WritePendingException();
default:
throw new IllegalStateException(stateString());
}
_written = written;
// Should we aggregate?
if (aggregate) {
lockedAcquireBuffer();
int filled = BufferUtil.fill(_aggregate.getByteBuffer(), b, off, len);
// return if we are not complete, not full and filled all the content
if (!flush) {
if (LOG.isDebugEnabled())
LOG.debug("write(array) {} aggregated !flush {}", stateString(), _aggregate);
return;
}
// adjust offset/length
off += filled;
len -= filled;
}
}
if (LOG.isDebugEnabled())
LOG.debug("write(array) {} last={} agg={} flush=true async={}, len={} {}", stateString(), last, aggregate, async, len, _aggregate);
if (async) {
// Do the asynchronous writing from the callback
new AsyncWrite(b, off, len, last).iterate();
return;
}
// Blocking write
try {
boolean complete = false;
// flush any content from the aggregate
if (_aggregate != null && _aggregate.hasRemaining()) {
ByteBuffer byteBuffer = _aggregate.getByteBuffer();
complete = last && len == 0;
channelWrite(byteBuffer, complete);
// should we fill aggregate again from the buffer?
if (len > 0 && !last && len <= _commitSize && len <= maximizeAggregateSpace()) {
BufferUtil.append(byteBuffer, b, off, len);
onWriteComplete(false, null);
return;
}
}
// write any remaining content in the buffer directly
if (len > 0) {
// write a buffer capacity at a time to avoid JVM pooling large direct buffers
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6210541
ByteBuffer view = ByteBuffer.wrap(b, off, len);
while (len > getBufferSize()) {
int p = view.position();
int l = p + getBufferSize();
view.limit(l);
channelWrite(view, false);
view.limit(p + len);
view.position(l);
len -= getBufferSize();
}
channelWrite(view, last);
} else if (last && !complete) {
channelWrite(BufferUtil.EMPTY_BUFFER, true);
}
onWriteComplete(last, null);
} catch (Throwable t) {
onWriteComplete(last, t);
throw t;
}
}
public void write(ByteBuffer buffer) throws IOException {
// This write always bypasses aggregate buffer
int len = BufferUtil.length(buffer);
boolean flush;
boolean last;
// Async or Blocking ?
boolean async;
try (AutoLock l = _channelState.lock()) {
checkWritable();
long written = _written + len;
last = _channel.getResponse().isAllContentWritten(written);
flush = last || len > 0 || (_aggregate != null && _aggregate.hasRemaining());
if (last && _state == State.OPEN)
_state = State.CLOSING;
switch(_apiState) {
case BLOCKING:
async = false;
_apiState = flush ? ApiState.BLOCKED : ApiState.BLOCKING;
break;
case ASYNC:
throw new IllegalStateException("isReady() not called: " + stateString());
case READY:
async = true;
_apiState = flush ? ApiState.PENDING : ApiState.ASYNC;
break;
case PENDING:
case UNREADY:
throw new WritePendingException();
default:
throw new IllegalStateException(stateString());
}
_written = written;
}
if (!flush)
return;
if (async) {
new AsyncWrite(buffer, last).iterate();
} else {
try {
// Blocking write
// flush any content from the aggregate
boolean complete = false;
if (_aggregate != null && _aggregate.hasRemaining()) {
complete = last && len == 0;
channelWrite(_aggregate.getByteBuffer(), complete);
}
// write any remaining content in the buffer directly
if (len > 0)
channelWrite(buffer, last);
else if (last && !complete)
channelWrite(BufferUtil.EMPTY_BUFFER, true);
onWriteComplete(last, null);
} catch (Throwable t) {
onWriteComplete(last, t);
throw t;
}
}
}
@Override
public void write(int b) throws IOException {
boolean flush;
boolean last;
// Async or Blocking ?
boolean async = false;
try (AutoLock l = _channelState.lock()) {
checkWritable();
long written = _written + 1;
int space = maximizeAggregateSpace();
last = _channel.getResponse().isAllContentWritten(written);
flush = last || space == 1;
if (last && _state == State.OPEN)
_state = State.CLOSING;
switch(_apiState) {
case BLOCKING:
_apiState = flush ? ApiState.BLOCKED : ApiState.BLOCKING;
break;
case ASYNC:
throw new IllegalStateException("isReady() not called: " + stateString());
case READY:
async = true;
_apiState = flush ? ApiState.PENDING : ApiState.ASYNC;
break;
case PENDING:
case UNREADY:
throw new WritePendingException();
default:
throw new IllegalStateException(stateString());
}
_written = written;
lockedAcquireBuffer();
BufferUtil.append(_aggregate.getByteBuffer(), (byte) b);
}
// Check if all written or full
if (!flush)
return;
if (async) {
// Do the asynchronous writing from the callback
new AsyncFlush(last).iterate();
} else {
try {
channelWrite(_aggregate.getByteBuffer(), last);
onWriteComplete(last, null);
} catch (Throwable t) {
onWriteComplete(last, t);
throw t;
}
}
}
@Override
public void print(String s) throws IOException {
print(s, false);
}
@Override
public void println(String s) throws IOException {
print(s, true);
}
private void print(String s, boolean eoln) throws IOException {
if (isClosed())
throw new IOException("Closed");
s = String.valueOf(s);
String charset = _channel.getResponse().getCharacterEncoding();
CharsetEncoder encoder = _encoder.take();
if (encoder == null || !encoder.charset().name().equalsIgnoreCase(charset)) {
encoder = Charset.forName(charset).newEncoder();
encoder.onMalformedInput(CodingErrorAction.REPLACE);
encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
} else {
encoder.reset();
}
RetainableByteBuffer out = getHttpChannel().getByteBufferPool().acquire((int) (1 + (s.length() + 2) * encoder.averageBytesPerChar()), false);
try {
CharBuffer in = CharBuffer.wrap(s);
CharBuffer crlf = eoln ? CharBuffer.wrap("\r\n") : null;
ByteBuffer byteBuffer = out.getByteBuffer();
BufferUtil.flipToFill(byteBuffer);
while (true) {
CoderResult result;
if (in.hasRemaining()) {
result = encoder.encode(in, byteBuffer, crlf == null);
if (result.isUnderflow())
if (crlf == null)
break;
else
continue;
} else if (crlf != null && crlf.hasRemaining()) {
result = encoder.encode(crlf, byteBuffer, true);
if (result.isUnderflow()) {
if (!encoder.flush(byteBuffer).isUnderflow())
result.throwException();
break;
}
} else
break;
if (result.isOverflow()) {
BufferUtil.flipToFlush(byteBuffer, 0);
RetainableByteBuffer bigger = _channel.getByteBufferPool().acquire(out.capacity() + s.length() + 2, out.isDirect());
BufferUtil.flipToFill(bigger.getByteBuffer());
bigger.getByteBuffer().put(byteBuffer);
out.release();
BufferUtil.flipToFill(bigger.getByteBuffer());
out = bigger;
byteBuffer = bigger.getByteBuffer();
continue;
}
result.throwException();
}
BufferUtil.flipToFlush(byteBuffer, 0);
write(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.remaining());
} finally {
out.release();
encoder.reset();
_encoder.offer(encoder);
}
}
/**
* Blocking send of whole content.
*
* @param content The whole content to send
* @throws IOException if the send fails
*/
public void sendContent(ByteBuffer content) throws IOException {
if (LOG.isDebugEnabled())
LOG.debug("sendContent({})", BufferUtil.toDetailString(content));
_written += content.remaining();
channelWrite(content, true);
}
/**
* Blocking send of stream content.
*
* @param in The stream content to send
* @throws IOException if the send fails
*/
public void sendContent(InputStream in) throws IOException {
try (Blocker blocker = _writeBlocker.acquire()) {
sendContent(in, blocker);
blocker.block();
}
}
/**
* Blocking send of channel content.
*
* @param in The channel content to send
* @throws IOException if the send fails
*/
public void sendContent(ReadableByteChannel in) throws IOException {
try (Blocker blocker = _writeBlocker.acquire()) {
sendContent(in, blocker);
blocker.block();
}
}
/**
* Blocking send of resource.
*
* @param resource The resource content to send
* @throws IOException if the send fails
*/
public void sendContent(Resource resource) throws IOException {
try (Blocker blocker = _writeBlocker.acquire()) {
sendContent(resource, blocker);
blocker.block();
}
}
/**
* Blocking send of HTTP content.
*
* @param content The HTTP content to send
* @throws IOException if the send fails
*/
public void sendContent(HttpContent content) throws IOException {
try (Blocker blocker = _writeBlocker.acquire()) {
sendContent(content, blocker);
blocker.block();
}
}
/**
* Asynchronous send of whole content.
*
* @param content The whole content to send
* @param callback The callback to use to notify success or failure
*/
public void sendContent(ByteBuffer content, final Callback callback) {
if (LOG.isDebugEnabled())
LOG.debug("sendContent(buffer={},{})", BufferUtil.toDetailString(content), callback);
if (prepareSendContent(content.remaining(), callback)) {
channelWrite(content, true, new Callback.Nested(callback) {
@Override
public void succeeded() {
onWriteComplete(true, null);
super.succeeded();
}
@Override
public void failed(Throwable x) {
onWriteComplete(true, x);
super.failed(x);
}
});
}
}
/**
* Asynchronous send of stream content.
* The stream will be closed after reading all content.
*
* @param in The stream content to send
* @param callback The callback to use to notify success or failure
*/
public void sendContent(InputStream in, Callback callback) {
if (LOG.isDebugEnabled())
LOG.debug("sendContent(stream={},{})", in, callback);
if (prepareSendContent(0, callback))
new InputStreamWritingCB(in, callback).iterate();
}
/**
* Asynchronous send of channel content.
* The channel will be closed after reading all content.
*
* @param in The channel content to send
* @param callback The callback to use to notify success or failure
*/
public void sendContent(ReadableByteChannel in, Callback callback) {
if (LOG.isDebugEnabled())
LOG.debug("sendContent(channel={},{})", in, callback);
if (prepareSendContent(0, callback))
new ReadableByteChannelWritingCB(in, callback).iterate();
}
/**
* Asynchronous send of whole resource.
*
* @param resource The resource content to send
* @param callback The callback to use to notify success or failure
*/
public void sendContent(Resource resource, Callback callback) {
try {
if (prepareSendContent(0, callback)) {
IOResources.copy(resource, (last, byteBuffer, cb) -> {
_written += byteBuffer.remaining();
channelWrite(byteBuffer, last, cb);
}, getSizedByteBufferPool(), 0L, -1L, new Callback.Nested(callback) {
@Override
public void succeeded() {
onWriteComplete(true, null);
super.succeeded();
}
@Override
public void failed(Throwable x) {
onWriteComplete(true, x);
super.failed(x);
}
});
}
} catch (Throwable x) {
if (LOG.isDebugEnabled())
LOG.debug("Unable to send resource {}", resource, x);
callback.failed(x);
}
}
/**
* Asynchronous send of HTTP content.
*
* @param httpContent The HTTP content to send
* @param callback The callback to use to notify success or failure
*/
public void sendContent(HttpContent httpContent, Callback callback) {
if (LOG.isDebugEnabled())
LOG.debug("sendContent(http={},{})", httpContent, callback);
try {
if (prepareSendContent(0, callback)) {
Content.Sink sink = (last, byteBuffer, cb) -> {
_written += byteBuffer.remaining();
channelWrite(byteBuffer, last, cb);
};
httpContent.writeTo(sink, 0L, -1L, new Callback.Nested(callback) {
@Override
public void succeeded() {
onWriteComplete(true, null);
super.succeeded();
}
@Override
public void failed(Throwable x) {
onWriteComplete(true, x);
super.failed(x);
}
});
}
} catch (Throwable x) {
if (LOG.isDebugEnabled())
LOG.debug("Unable to send http content {}", httpContent, x);
_channel.abort(x);
callback.failed(x);
}
}
private boolean prepareSendContent(int len, Callback callback) {
try (AutoLock l = _channelState.lock()) {
if (_aggregate != null && _aggregate.hasRemaining()) {
callback.failed(new IOException("cannot sendContent() after write()"));
return false;
}
if (_channel.isCommitted()) {
callback.failed(new IOException("cannot sendContent(), output already committed"));
return false;
}
switch(_state) {
case CLOSED:
case CLOSING:
callback.failed(new EofException("Closed"));
return false;
default:
_state = State.CLOSING;
break;
}
if (_onError != null) {
callback.failed(_onError);
return false;
}
if (_apiState != ApiState.BLOCKING)
throw new IllegalStateException(stateString());
_apiState = ApiState.PENDING;
if (len > 0)
_written += len;
return true;
}
}
public int getBufferSize() {
return _bufferSize;
}
public void setBufferSize(int size) {
_bufferSize = size;
_commitSize = size;
}
public void recycle() {
try (AutoLock l = _channelState.lock()) {
lockedReleaseBuffer();
_state = State.OPEN;
_apiState = ApiState.BLOCKING;
// Stay closed until next request
_softClose = true;
_interceptor = _channel;
HttpConfiguration config = _channel.getHttpConfiguration();
_bufferSize = config.getOutputBufferSize();
_commitSize = config.getOutputAggregationSize();
if (_commitSize > _bufferSize)
_commitSize = _bufferSize;
_written = 0;
_writeListener = null;
_onError = null;
_firstByteNanoTime = -1;
_closedCallback = null;
}
}
public void resetBuffer() {
try (AutoLock l = _channelState.lock()) {
_interceptor.resetBuffer();
if (_aggregate != null)
_aggregate.clear();
_written = 0;
}
}
@Override
public void setWriteListener(WriteListener writeListener) {
if (!_channel.getState().isAsync())
throw new IllegalStateException("!ASYNC: " + stateString());
boolean wake;
try (AutoLock l = _channelState.lock()) {
if (_apiState != ApiState.BLOCKING)
throw new IllegalStateException("!OPEN" + stateString());
_apiState = ApiState.READY;
_writeListener = writeListener;
wake = _channel.getState().onWritePossible();
}
if (wake)
_channel.execute(_channel);
}
@Override
public boolean isReady() {
try (AutoLock l = _channelState.lock()) {
switch(_apiState) {
case BLOCKING:
case READY:
return true;
case ASYNC:
_apiState = ApiState.READY;
return true;
case PENDING:
_apiState = ApiState.UNREADY;
return false;
case BLOCKED:
case UNREADY:
return false;
default:
throw new IllegalStateException(stateString());
}
}
}
@Override
public void run() {
Throwable error = null;
try (AutoLock l = _channelState.lock()) {
if (_onError != null) {
error = _onError;
_onError = null;
}
}
try {
if (error == null) {
if (LOG.isDebugEnabled())
LOG.debug("onWritePossible");
_writeListener.onWritePossible();
return;
}
} catch (Throwable t) {
error = t;
}
try {
if (LOG.isDebugEnabled())
LOG.debug("onError", error);
_writeListener.onError(error);
} catch (Throwable t) {
if (LOG.isDebugEnabled()) {
t.addSuppressed(error);
LOG.debug("Failed in call onError on {}", _writeListener, t);
}
} finally {
IO.close(this);
}
}
private String stateString() {
return String.format("s=%s,api=%s,sc=%b,e=%s", _state, _apiState, _softClose, _onError);
}
@Override
public String toString() {
try (AutoLock l = _channelState.lock()) {
return String.format("%s@%x{%s}", this.getClass().getSimpleName(), hashCode(), stateString());
}
}
private abstract class ChannelWriteCB extends IteratingCallback {
final boolean _last;
private ChannelWriteCB(boolean last) {
_last = last;
}
@Override
public InvocationType getInvocationType() {
return InvocationType.NON_BLOCKING;
}
@Override
protected void onCompleteSuccess() {
onWriteComplete(_last, null);
}
@Override
protected void onFailure(Throwable e) {
onWriteComplete(_last, e);
}
@Override
protected void onCompleteFailure(Throwable cause) {
try (AutoLock l = _channelState.lock()) {
lockedReleaseBuffer();
}
}
}
private abstract class NestedChannelWriteCB extends ChannelWriteCB {
private final Callback _callback;
private NestedChannelWriteCB(Callback callback, boolean last) {
super(last);
_callback = callback;
}
@Override
public InvocationType getInvocationType() {
return _callback.getInvocationType();
}
@Override
protected void onCompleteSuccess() {
try {
super.onCompleteSuccess();
} finally {
_callback.succeeded();
}
}
@Override
protected void onFailure(Throwable e) {
try {
super.onFailure(e);
} catch (Throwable t) {
ExceptionUtil.addSuppressedIfNotAssociated(e, t);
} finally {
_callback.failed(e);
}
}
}
private class AsyncFlush extends ChannelWriteCB {
private volatile boolean _flushed;
private AsyncFlush(boolean last) {
super(last);
}
@Override
protected Action process() {
if (_aggregate != null && _aggregate.hasRemaining()) {
_flushed = true;
channelWrite(_aggregate.getByteBuffer(), false, this);
return Action.SCHEDULED;
}
if (!_flushed) {
_flushed = true;
channelWrite(BufferUtil.EMPTY_BUFFER, false, this);
return Action.SCHEDULED;
}
return Action.SUCCEEDED;
}
}
private class AsyncWrite extends ChannelWriteCB {
private final ByteBuffer _buffer;
private final ByteBuffer _slice;
private final int _len;
private boolean _completed;
private AsyncWrite(byte[] b, int off, int len, boolean last) {
super(last);
_buffer = ByteBuffer.wrap(b, off, len);
_len = len;
// always use a view for large byte arrays to avoid JVM pooling large direct buffers
_slice = _len < getBufferSize() ? null : _buffer.duplicate();
}
private AsyncWrite(ByteBuffer buffer, boolean last) {
super(last);
_buffer = buffer;
_len = buffer.remaining();
// Use a slice buffer for large indirect to avoid JVM pooling large direct buffers
if (_buffer.isDirect() || _len < getBufferSize())
_slice = null;
else {
_slice = _buffer.duplicate();
}
}
@Override
protected Action process() {
// flush any content from the aggregate
if (_aggregate != null && _aggregate.hasRemaining()) {
_completed = _len == 0;
channelWrite(_aggregate.getByteBuffer(), _last && _completed, this);
return Action.SCHEDULED;
}
// Can we just aggregate the remainder?
if (!_last && _aggregate != null && _len < maximizeAggregateSpace() && _len < _commitSize) {
ByteBuffer byteBuffer = _aggregate.getByteBuffer();
int position = BufferUtil.flipToFill(byteBuffer);
BufferUtil.put(_buffer, byteBuffer);
BufferUtil.flipToFlush(byteBuffer, position);
return Action.SUCCEEDED;
}
// Is there data left to write?
if (_buffer.hasRemaining()) {
// if there is no slice, just write it
if (_slice == null) {
_completed = true;
channelWrite(_buffer, _last, this);
return Action.SCHEDULED;
}
// otherwise take a slice
int p = _buffer.position();
int l = Math.min(getBufferSize(), _buffer.remaining());
int pl = p + l;
_slice.limit(pl);
_buffer.position(pl);
_slice.position(p);
_completed = !_buffer.hasRemaining();
channelWrite(_slice, _last && _completed, this);
return Action.SCHEDULED;
}
// all content written, but if we have not yet signal completion, we
// need to do so
if (_last && !_completed) {
_completed = true;
channelWrite(BufferUtil.EMPTY_BUFFER, true, this);
return Action.SCHEDULED;
}
if (LOG.isDebugEnabled() && _completed)
LOG.debug("EOF of {}", this);
return Action.SUCCEEDED;
}
}
/**
* An iterating callback that will take content from an
* InputStream and write it to the associated {@link HttpChannel}.
* A non direct buffer of size {@link HttpOutput#getBufferSize()} is used.
* This callback is passed to the {@link HttpChannel#write(ByteBuffer, boolean, Callback)} to
* be notified as each buffer is written and only once all the input is consumed will the
* wrapped {@link Callback#succeeded()} method be called.
*/
private class InputStreamWritingCB extends NestedChannelWriteCB {
private final InputStream _in;
private final RetainableByteBuffer _buffer;
private boolean _eof;
private boolean _closed;
private InputStreamWritingCB(InputStream in, Callback callback) {
super(callback, true);
_in = in;
// Reading from InputStream requires byte[], don't use direct buffers.
_buffer = _channel.getByteBufferPool().acquire(getBufferSize(), false);
}
@Override
protected Action process() throws Exception {
// Only return if EOF has previously been read and thus
// a write done with EOF=true
if (_eof) {
if (LOG.isDebugEnabled())
LOG.debug("EOF of {}", this);
if (!_closed)
_closed = true;
return Action.SUCCEEDED;
}
ByteBuffer byteBuffer = _buffer.getByteBuffer();
// Read until buffer full or EOF
int len = 0;
while (len < byteBuffer.capacity() && !_eof) {
int r = _in.read(byteBuffer.array(), byteBuffer.arrayOffset() + len, byteBuffer.capacity() - len);
if (r < 0)
_eof = true;
else
len += r;
}
// write what we have
byteBuffer.position(0);
byteBuffer.limit(len);
_written += len;
channelWrite(byteBuffer, _eof, this);
return Action.SCHEDULED;
}
@Override
protected void onCompleteSuccess() {
super.onCompleteSuccess();
_buffer.release();
IO.close(_in);
}
@Override
protected void onFailure(Throwable cause) {
super.onFailure(cause);
IO.close(_in);
}
@Override
public void onCompleteFailure(Throwable x) {
super.onCompleteFailure(x);
_buffer.release();
}
}
/**
* An iterating callback that will take content from a
* ReadableByteChannel and write it to the {@link HttpChannel}.
* A {@link ByteBuffer} of size {@link HttpOutput#getBufferSize()} is used that will be direct if
* {@link HttpChannel#isUseOutputDirectByteBuffers()} is true.
* This callback is passed to the {@link HttpChannel#write(ByteBuffer, boolean, Callback)} to
* be notified as each buffer is written and only once all the input is consumed will the
* wrapped {@link Callback#succeeded()} method be called.
*/
private class ReadableByteChannelWritingCB extends NestedChannelWriteCB {
private final ReadableByteChannel _in;
private final RetainableByteBuffer _buffer;
private boolean _eof;
private boolean _closed;
private ReadableByteChannelWritingCB(ReadableByteChannel in, Callback callback) {
super(callback, true);
_in = in;
_buffer = _channel.getByteBufferPool().acquire(getBufferSize(), _channel.isUseOutputDirectByteBuffers());
}
@Override
protected Action process() throws Exception {
// Only return if EOF has previously been read and thus
// a write done with EOF=true
if (_eof) {
if (LOG.isDebugEnabled())
LOG.debug("EOF of {}", this);
if (!_closed)
_closed = true;
return Action.SUCCEEDED;
}
ByteBuffer byteBuffer = _buffer.getByteBuffer();
// Read from stream until buffer full or EOF
BufferUtil.clearToFill(byteBuffer);
while (byteBuffer.hasRemaining() && !_eof) {
_eof = (_in.read(byteBuffer)) < 0;
}
// write what we have
BufferUtil.flipToFlush(byteBuffer, 0);
_written += byteBuffer.remaining();
channelWrite(byteBuffer, _eof, this);
return Action.SCHEDULED;
}
@Override
protected void onCompleteSuccess() {
super.onCompleteSuccess();
_buffer.release();
IO.close(_in);
}
@Override
protected void onFailure(Throwable cause) {
super.onFailure(cause);
IO.close(_in);
}
@Override
public void onCompleteFailure(Throwable x) {
super.onCompleteFailure(x);
_buffer.release();
}
}
private static class WriteBlocker extends SharedBlockingCallback {
private final HttpChannel _channel;
private WriteBlocker(HttpChannel channel) {
_channel = channel;
}
}
private class WriteCompleteCB implements Callback {
@Override
public void succeeded() {
onWriteComplete(true, null);
}
@Override
public void failed(Throwable x) {
onWriteComplete(true, x);
}
@Override
public InvocationType getInvocationType() {
return InvocationType.NON_BLOCKING;
}
}
}