package com.firefly.codec.http2.stream;
import com.firefly.Version;
import com.firefly.codec.http2.frame.DataFrame;
import com.firefly.codec.http2.frame.DisconnectFrame;
import com.firefly.codec.http2.frame.Frame;
import com.firefly.codec.http2.frame.HeadersFrame;
import com.firefly.codec.http2.model.HttpFields;
import com.firefly.codec.http2.model.HttpHeader;
import com.firefly.codec.http2.model.HttpVersion;
import com.firefly.codec.http2.model.MetaData;
import com.firefly.utils.concurrent.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.function.Supplier;
abstract public class AbstractHTTP2OutputStream extends HTTPOutputStream {
protected static final Logger log = LoggerFactory.getLogger("firefly-system");
public static final String X_POWERED_BY_VALUE = "Firefly " + Version.value;
public static final String SERVER_VALUE = "Firefly " + Version.value;
protected boolean isChunked;
private long size;
private long contentLength;
private boolean isWriting;
private LinkedList<Frame> frames = new LinkedList<>();
private FrameCallback frameCallback = new FrameCallback();
private DataFrame currentDataFrame;
public AbstractHTTP2OutputStream(MetaData info, boolean clientMode) {
super(info, clientMode);
}
@Override
public void commit() throws IOException {
commit(false);
}
@Override
public synchronized void write(ByteBuffer data) throws IOException {
if (closed)
return;
if (data == null || !data.hasRemaining())
return;
if (!committed) {
commit(false);
}
boolean endStream = false;
if (!isChunked) {
size += data.remaining();
log.debug("http2 output size: {}, content length: {}", size, contentLength);
if (size >= contentLength) {
endStream = true;
}
}
final Stream stream = getStream();
final DataFrame frame = new DataFrame(stream.getId(), data, endStream);
writeFrame(frame);
}
public synchronized void writeFrame(Frame frame) {
switch (frame.getType()) {
case DATA:
if (!committed)
throw new IllegalStateException("the output stream is not committed");
DataFrame dataFrame = (DataFrame) frame;
if (isChunked) {
if (dataFrame.isEndStream()) {
if (currentDataFrame == null) {
writeDataFrame(dataFrame);
} else {
writeDataFrame(currentDataFrame);
writeDataFrame(dataFrame);
}
} else {
if (currentDataFrame == null) {
currentDataFrame = dataFrame;
} else {
writeDataFrame(currentDataFrame);
currentDataFrame = dataFrame;
}
}
} else {
writeDataFrame(dataFrame);
}
break;
case HEADERS:
writeHeadersFrame((HeadersFrame) frame);
break;
case DISCONNECT:
if (isChunked) {
if (currentDataFrame != null) {
if (!currentDataFrame.isEndStream()) {
final Supplier<HttpFields> trailers = info.getTrailerSupplier();
if (trailers == null) {
DataFrame theLastDataFrame = new DataFrame(currentDataFrame.getStreamId(), currentDataFrame.getData(), true);
writeDataFrame(theLastDataFrame);
currentDataFrame = null;
} else {
DataFrame theLastDataFrame = new DataFrame(currentDataFrame.getStreamId(), currentDataFrame.getData(), false);
writeDataFrame(theLastDataFrame);
currentDataFrame = null;
writeTrailer();
}
} else {
throw new IllegalStateException("the end data stream is cached");
}
} else {
throw new IllegalStateException("the cached data stream is null");
}
} else {
throw new IllegalArgumentException(
"the frame type is error, only the chunked encoding can accept disconnect frame, current frame type is "
+ frame.getType());
}
break;
default:
throw new IllegalArgumentException("the frame type is error, the type is " + frame.getType());
}
}
protected synchronized void writeDataFrame(DataFrame dataFrame) {
closed = dataFrame.isEndStream();
if (isWriting) {
frames.offer(dataFrame);
} else {
if (log.isDebugEnabled()) {
log.debug("the stream {} writes a frame {}, remaining frames are {}", dataFrame.getStreamId(), dataFrame, frames.toString());
}
isWriting = true;
getStream().data(dataFrame, frameCallback);
}
}
protected synchronized void writeHeadersFrame(HeadersFrame headersFrame) {
closed = headersFrame.isEndStream();
if (isWriting) {
frames.offer(headersFrame);
} else {
if (log.isDebugEnabled()) {
log.debug("the stream {} writes a frame {}", headersFrame.getStreamId(), headersFrame);
}
isWriting = true;
getStream().headers(headersFrame, frameCallback);
}
}
@Override
public synchronized void close() throws IOException {
if (closed)
return;
log.debug("http2 output stream is closing");
if (!committed) {
commit(true);
} else {
if (isChunked) {
log.debug("output the last data frame to end stream");
writeFrame(new DisconnectFrame());
} else {
closed = true;
}
}
}
protected synchronized void commit(final boolean endStream) throws IOException {
if (closed)
return;
if (committed)
return;
// does use chunked encoding or content length ?
contentLength = info.getFields().getLongField(HttpHeader.CONTENT_LENGTH.asString());
if (endStream) {
if (log.isDebugEnabled()) {
log.debug("stream {} commits header and closes it", getStream().getId());
}
isChunked = false;
} else {
isChunked = (contentLength <= 0);
}
if (log.isDebugEnabled()) {
log.debug("is stream {} using chunked encoding ? {}", getStream().getId(), isChunked);
}
final Supplier<HttpFields> trailers = info.getTrailerSupplier();
final Stream stream = getStream();
committed = true;
if (trailers == null) {
HeadersFrame headersFrame = new HeadersFrame(stream.getId(), info, null, endStream);
if (log.isDebugEnabled()) {
log.debug("stream {} commits the header frame {}", stream.getId(), headersFrame);
}
writeFrame(headersFrame);
} else {
HeadersFrame headersFrame = new HeadersFrame(stream.getId(), info, null, false);
if (log.isDebugEnabled()) {
log.debug("stream {} commits the header frame {}", stream.getId(), headersFrame);
}
writeFrame(headersFrame);
if (endStream) {
writeTrailer();
}
}
}
private void writeTrailer() {
final Supplier<HttpFields> trailers = info.getTrailerSupplier();
final Stream stream = getStream();
MetaData trailerMetaData = new MetaData(info.getHttpVersion(), trailers.get());
HeadersFrame trailer = new HeadersFrame(stream.getId(), trailerMetaData, null, true);
if (log.isDebugEnabled()) {
log.debug("stream {} write the trailer frame {}", stream.getId(), trailer);
}
writeFrame(trailer);
}
private class FrameCallback implements Callback {
@Override
public void succeeded() {
synchronized (AbstractHTTP2OutputStream.this) {
isWriting = false;
final Frame frame = frames.poll();
if (frame != null) {
switch (frame.getType()) {
case DATA:
writeDataFrame((DataFrame) frame);
break;
case HEADERS:
writeHeadersFrame((HeadersFrame) frame);
break;
default:
throw new IllegalArgumentException("the frame type is error, the type is " + frame.getType());
}
} else {
isWriting = false;
}
if (log.isDebugEnabled()) {
log.debug("the stream {} outputs http2 frame successfully, and the queue size is {}",
getStream().getId(), frames.size());
}
}
}
@Override
public void failed(Throwable x) {
synchronized (AbstractHTTP2OutputStream.this) {
log.error("the stream {} outputs http2 frame unsuccessfully ", x, getStream().getId());
isWriting = false;
}
}
}
abstract protected Stream getStream();
}