package com.linkedin.databus2.core.container.netty;
/*
*
* Copyright 2013 LinkedIn Corp. All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.apache.log4j.Logger;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelConfig;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.socket.nio.NioSocketChannelConfig;
import org.jboss.netty.handler.codec.http.DefaultHttpChunk;
import org.jboss.netty.handler.codec.http.DefaultHttpChunkTrailer;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpChunkTrailer;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import com.linkedin.databus2.core.container.ChunkedWritableByteChannel;
public class ChunkedBodyWritableByteChannel implements ChunkedWritableByteChannel
{
public static final String MODULE = ChunkedBodyWritableByteChannel.class.getName();
public static final Logger LOG = Logger.getLogger(MODULE);
public static final String RESPONSE_CODE_FOOTER_NAME = "x-databus-response-code";
private final Channel _channel;
/** Temporarily stores the HTTP response headers until the first byte of the body is sent.
* This lets response producers to set headers. If set to null, the response headers have been
* sent.
*/
private HttpResponse _response;
private HttpResponseStatus _responseCode;
private boolean _open = true;
private HttpChunk _chunkReuse = null;
private HttpChunkTrailer _trailer = null;
public ChunkedBodyWritableByteChannel(Channel channel, HttpResponse response)
{
_channel = channel;
_responseCode = response.getStatus();
_response = response;
ChannelConfig channelConfig = _channel.getConfig();
if (channelConfig instanceof NioSocketChannelConfig)
{
NioSocketChannelConfig nioSocketConfig = (NioSocketChannelConfig)channelConfig;
//FIXME: add a config for the high water-mark
nioSocketConfig.setWriteBufferLowWaterMark(16 * 1024);
nioSocketConfig.setWriteBufferHighWaterMark(64 * 1024);
}
}
@Override
public int write(ByteBuffer buffer) throws IOException
{
if (null != _response)
{
_response.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
_response.setChunked(true);
writeToChannel(_response);
_response = null;
}
//We need to lie to netty that the buffer is BIG_ENDIAN event if it is LITTLE_ENDIAN (see DDS-1212)
ByteOrder bufferOrder = buffer.order();
ByteBuffer realBuffer = bufferOrder == ByteOrder.BIG_ENDIAN
? buffer : buffer.slice().order(ByteOrder.BIG_ENDIAN);
if (null == _chunkReuse)
{
_chunkReuse = new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(realBuffer));
}
else
{
_chunkReuse.setContent(ChannelBuffers.wrappedBuffer(realBuffer));
}
int bytesWritten = realBuffer.remaining();
writeToChannel(_chunkReuse); //We assume this either writes the whole buffer or else throws (no partial writes)
//DDSDBUS-1363: WritableByteChannel.write() contract requires both (1) return of bytes written and (2) update of
//position. Without the latter, NIO loops forever.
realBuffer.position(realBuffer.limit());
//DDS-1212
if (bufferOrder == ByteOrder.LITTLE_ENDIAN) buffer.position(realBuffer.position());
return bytesWritten;
}
@Override
public void close() throws IOException
{
if (_open)
{
_open = false;
if (null != _response)
{
_response.setHeader(HttpHeaders.Names.CONTENT_LENGTH,
Long.valueOf(_response.getContent().readableBytes()));
writeToChannel(_response, 10);
_response = null;
}
else
{
if (null != _trailer)
{
writeToChannel(_trailer, 1);
_trailer = null;
}
else
{
writeToChannel(HttpChunk.LAST_CHUNK, 1);
}
}
}
}
@Override
public void addMetadata(String name, Object value)
{
if (null != _response)
{
_response.addHeader(name, value);
}
else
{
if (null == _trailer)
{
_trailer = new DefaultHttpChunkTrailer();
}
_trailer.addHeader(name, value);
}
}
@Override
public void setMetadata(String name, Object value)
{
if (null != _response)
{
_response.setHeader(name, value);
}
else
{
if (null == _trailer)
{
_trailer = new DefaultHttpChunkTrailer();
}
_trailer.setHeader(name, value);
}
}
@Override
public void removeMetadata(String name)
{
if (null != _response)
{
_response.removeHeader(name);
}
else if (null != _trailer)
{
_trailer.removeHeader(name);
}
}
private void writeToChannel(Object o, int flushSize) throws IOException
{
ChannelFuture channelFuture = _channel.write(o);
if (flushSize > 0 && !_channel.isWritable())
{
ChannelConfig channelConfig = _channel.getConfig();
if (channelConfig instanceof NioSocketChannelConfig)
{
NioSocketChannelConfig nioSocketConfig = (NioSocketChannelConfig)channelConfig;
nioSocketConfig.setWriteBufferLowWaterMark(flushSize);
nioSocketConfig.setWriteBufferHighWaterMark(flushSize);
}
}
awaitChannelFuture(channelFuture);
if (! channelFuture.isSuccess())
{
throw new IOException(channelFuture.getCause());
}
}
private void writeToChannel(Object o) throws IOException
{
writeToChannel(o, 0);
}
private void awaitChannelFuture(ChannelFuture channelFuture)
{
boolean done = channelFuture.isDone();
while (!done)
{
try
{
channelFuture.await();
done = channelFuture.isDone();
}
catch (InterruptedException ie)
{//do nothing
}
}
}
@Override
public boolean isOpen()
{
return _open;
}
@Override
public HttpResponseStatus getResponseCode()
{
return _responseCode;
}
@Override
public void setResponseCode(HttpResponseStatus responseCode)
{
_responseCode = responseCode;
if (null != _response)
{
_response.setStatus(responseCode);
}
else
{
setMetadata(RESPONSE_CODE_FOOTER_NAME, Integer.toString(responseCode.getCode()));
}
}
@Override
public Channel getRawChannel()
{
return _channel;
}
}