/**
* Copyright 2013 Apigee Corporation.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.apigee.trireme.container.netty;
import io.apigee.trireme.core.internal.Charsets;
import io.apigee.trireme.net.spi.HttpFuture;
import io.apigee.trireme.net.spi.HttpResponseAdapter;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.DefaultChannelPromise;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Map;
public class NettyHttpResponse
extends NettyHttpMessage
implements HttpResponseAdapter
{
private static final Logger log = LoggerFactory.getLogger(NettyHttpResponse.class);
private final HttpResponse response;
private final NettyHttpServer server;
private boolean keepAlive;
private final boolean isTls;
private ArrayList<Map.Entry<String, String>> trailers;
public NettyHttpResponse(HttpResponse resp, SocketChannel channel,
boolean keepAliveRequested, boolean isTls,
NettyHttpServer server)
{
super(resp, channel);
this.response = resp;
this.keepAlive = keepAliveRequested;
this.server = server;
this.isTls = isTls;
}
@Override
public int getStatusCode()
{
return response.getStatus().code();
}
@Override
public void setStatusCode(int code)
{
response.setStatus(HttpResponseStatus.valueOf(code));
}
private void calculateKeepAlive(boolean lastChunk)
{
if (isOlderHttpVersion()) {
// HTTP 1.0 -- must close at end if no content length
if (lastChunk) {
if (!response.headers().contains("Content-Length")) {
keepAlive = false;
}
} else {
keepAlive = false;
}
} else {
// HTTP 1.1 -- we can use chunking
if (lastChunk && (trailers == null)) {
// We can send it all in one big chunk, but only if no trailers
if (!response.headers().contains("Content-Length") &&
!response.headers().contains("Transfer-Encoding")) {
response.headers().set("Content-Length", (data == null ? 0 : data.remaining()));
}
} else {
// We must use chunking
if (!response.headers().contains("Transfer-Encoding") &&
!response.headers().contains("Content-Length")) {
response.headers().set("Transfer-Encoding", "chunked");
}
}
}
String connHeader = response.headers().get("Connection");
if (server.isClosing()) {
keepAlive = false;
} else if ((connHeader != null) && "close".equalsIgnoreCase(connHeader)) {
keepAlive = false;
}
if (!keepAlive && (connHeader == null)) {
response.headers().add("Connection", "close");
}
}
private void shutDown()
{
if (log.isDebugEnabled()) {
log.debug("Shutting down HTTP output. TLS = {}", isTls);
}
if (isTls) {
channel.close();
} else {
channel.shutdownOutput();
}
}
@Override
public HttpFuture send(boolean lastChunk)
{
calculateKeepAlive(lastChunk);
if (log.isDebugEnabled()) {
log.debug("send: sending HTTP response {}", response);
}
ChannelFuture future = channel.write(response);
if (data != null) {
if (log.isDebugEnabled()) {
log.debug("send: Sending HTTP chunk with data {}", data);
}
DefaultHttpContent chunk =
new DefaultHttpContent(NettyServer.copyBuffer(data));
future = channel.write(chunk);
}
if (lastChunk) {
future = sendLastChunk();
}
channel.flush();
if (lastChunk && !keepAlive) {
shutDown();
}
return new NettyHttpFuture(future);
}
@Override
public HttpFuture sendChunk(ByteBuffer buf, boolean lastChunk)
{
ChannelFuture future = null;
if (buf != null) {
if (log.isDebugEnabled()) {
log.debug("sendChunk: Sending HTTP chunk {}", buf);
}
DefaultHttpContent chunk =
new DefaultHttpContent(NettyServer.copyBuffer(buf));
future = channel.write(chunk);
}
if (lastChunk) {
future = sendLastChunk();
}
channel.flush();
if (lastChunk && !keepAlive) {
shutDown();
}
if (future == null) {
DefaultChannelPromise doneFuture = new DefaultChannelPromise(channel);
doneFuture.setSuccess();
future = doneFuture;
}
return new NettyHttpFuture(future);
}
@Override
public void fatalError(String message, String stack)
{
if (log.isDebugEnabled()) {
log.debug("Sending HTTP error due to script error {}", message);
}
StringBuilder msg = new StringBuilder(message);
if (stack != null) {
msg.append('\n');
msg.append(stack);
}
ByteBuf data = Unpooled.copiedBuffer(msg, Charsets.UTF8);
response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
response.headers().add("Content-Type", "text/plain");
response.headers().add("Content-Length", data.readableBytes());
calculateKeepAlive(true);
channel.write(response);
DefaultHttpContent chunk = new DefaultHttpContent(data);
channel.write(chunk);
sendLastChunk();
channel.flush();
if (!keepAlive) {
shutDown();
}
}
private ChannelFuture sendLastChunk()
{
if (log.isDebugEnabled()) {
log.debug("send: Sending last HTTP chunk");
}
DefaultLastHttpContent chunk = new DefaultLastHttpContent();
if ((trailers != null) && !isOlderHttpVersion()) {
for (Map.Entry<String, String> t : trailers) {
chunk.trailingHeaders().add(t.getKey(), t.getValue());
}
}
ChannelFuture ret = channel.write(chunk);
return ret;
}
@Override
public void setTrailer(String name, String value)
{
if (trailers == null) {
trailers = new ArrayList<Map.Entry<String, String>>();
}
trailers.add(new AbstractMap.SimpleEntry<String, String>(name, value));
}
@Override
public void destroy()
{
channel.close();
}
}