package com.koushikdutta.async.http.server;
import android.text.TextUtils;
import com.koushikdutta.async.AsyncServer;
import com.koushikdutta.async.AsyncSocket;
import com.koushikdutta.async.BufferedDataSink;
import com.koushikdutta.async.ByteBufferList;
import com.koushikdutta.async.DataSink;
import com.koushikdutta.async.Util;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.WritableCallback;
import com.koushikdutta.async.http.AsyncHttpHead;
import com.koushikdutta.async.http.HttpUtil;
import com.koushikdutta.async.http.filter.ChunkedOutputFilter;
import com.koushikdutta.async.http.libcore.RawHeaders;
import com.koushikdutta.async.http.libcore.ResponseHeaders;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse {
private RawHeaders mRawHeaders = new RawHeaders();
private long mContentLength = -1;
private ResponseHeaders mHeaders = new ResponseHeaders(null, mRawHeaders);
@Override
public ResponseHeaders getHeaders() {
return mHeaders;
}
public AsyncSocket getSocket() {
return mSocket;
}
AsyncSocket mSocket;
AsyncHttpServerRequestImpl mRequest;
AsyncHttpServerResponseImpl(AsyncSocket socket, AsyncHttpServerRequestImpl req) {
mSocket = socket;
mRequest = req;
if (HttpUtil.isKeepAlive(req.getHeaders().getHeaders()))
mRawHeaders.set("Connection", "Keep-Alive");
}
@Override
public void write(ByteBuffer bb) {
if (bb.remaining() == 0)
return;
writeInternal(bb);
}
@Override
public void write(ByteBufferList bb) {
if (bb.remaining() == 0)
return;
writeInternal(bb);
}
private void writeInternal(ByteBuffer bb) {
assert !mEnded;
if (!mHasWritten) {
initFirstWrite();
return;
}
mSink.write(bb);
}
private void writeInternal(ByteBufferList bb) {
assert !mEnded;
if (!mHasWritten) {
initFirstWrite();
return;
}
mSink.write(bb);
}
boolean mHasWritten = false;
DataSink mSink;
void initFirstWrite() {
if (mHasWritten)
return;
mHasWritten = true;
assert null != mRawHeaders.getStatusLine();
String currentEncoding = mRawHeaders.get("Transfer-Encoding");
if ("".equals(currentEncoding))
mRawHeaders.removeAll("Transfer-Encoding");
boolean canUseChunked = ("Chunked".equalsIgnoreCase(currentEncoding) || currentEncoding == null)
&& !"close".equalsIgnoreCase(mRawHeaders.get("Connection"));
if (mContentLength < 0) {
String contentLength = mRawHeaders.get("Content-Length");
if (!TextUtils.isEmpty(contentLength))
mContentLength = Integer.valueOf(contentLength);
}
if (mContentLength < 0 && canUseChunked) {
mRawHeaders.set("Transfer-Encoding", "Chunked");
mSink = new ChunkedOutputFilter(mSocket);
}
else {
mSink = mSocket;
}
writeHeadInternal();
}
@Override
public void setWriteableCallback(WritableCallback handler) {
initFirstWrite();
mSink.setWriteableCallback(handler);
}
@Override
public WritableCallback getWriteableCallback() {
initFirstWrite();
return mSink.getWriteableCallback();
}
@Override
public void end() {
if ("Chunked".equalsIgnoreCase(mRawHeaders.get("Transfer-Encoding"))) {
initFirstWrite();
((ChunkedOutputFilter)mSink).setMaxBuffer(Integer.MAX_VALUE);
mSink.write(new ByteBufferList());
onEnd();
}
else if (!mHasWritten) {
if (!mRequest.getMethod().equalsIgnoreCase(AsyncHttpHead.METHOD))
send("text/html", "");
else {
writeHead();
onEnd();
}
}
}
private boolean mHeadWritten = false;
@Override
public void writeHead() {
initFirstWrite();
}
private void writeHeadInternal() {
assert !mHeadWritten;
mHeadWritten = true;
Util.writeAll(mSocket, mRawHeaders.toHeaderString().getBytes(), new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
// TODO: HACK!!!
// this really needs to be fixed. Not sure how to deal w/ writehead and
// first write
if (mSink instanceof BufferedDataSink)
((BufferedDataSink)mSink).setDataSink(mSocket);
WritableCallback writableCallback = getWriteableCallback();
if (writableCallback != null)
writableCallback.onWriteable();
}
});
}
@Override
public void setContentType(String contentType) {
assert !mHeadWritten;
mRawHeaders.set("Content-Type", contentType);
}
@Override
public void send(String contentType, final String string) {
try {
if (mRawHeaders.getStatusLine() == null)
responseCode(200);
assert mContentLength < 0;
byte[] bytes = string.getBytes("UTF-8");
mContentLength = bytes.length;
mRawHeaders.set("Content-Length", Integer.toString(bytes.length));
mRawHeaders.set("Content-Type", contentType);
Util.writeAll(this, string.getBytes(), new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
onEnd();
}
});
}
catch (UnsupportedEncodingException e) {
assert false;
}
}
boolean mEnded;
protected void onEnd() {
mEnded = true;
}
protected void report(Exception e) {
}
@Override
public void send(String string) {
responseCode(200);
String contentType = mRawHeaders.get("Content-Type");
if (contentType == null)
contentType = "text/html; charset=utf8";
send(contentType, string);
}
@Override
public void send(JSONObject json) {
send("application/json; charset=utf8", json.toString());
}
@Override
public void sendStream(InputStream inputStream, long totalLength) {
long start = 0;
long end = totalLength - 1;
String range = mRequest.getHeaders().getHeaders().get("Range");
if (range != null) {
String[] parts = range.split("=");
if (parts.length != 2 || !"bytes".equals(parts[0])) {
// Requested range not satisfiable
responseCode(416);
end();
return;
}
parts = parts[1].split("-");
try {
if (parts.length > 2)
throw new MalformedRangeException();
if (!TextUtils.isEmpty(parts[0]))
start = Long.parseLong(parts[0]);
if (parts.length == 2 && !TextUtils.isEmpty(parts[1]))
end = Long.parseLong(parts[1]);
else
end = totalLength - 1;
responseCode(206);
getHeaders().getHeaders().set("Content-Range", String.format("bytes %d-%d/%d", start, end, totalLength));
}
catch (Exception e) {
responseCode(416);
end();
return;
}
}
try {
if (start != inputStream.skip(start))
throw new StreamSkipException("skip failed to skip requested amount");
mContentLength = end - start + 1;
mRawHeaders.set("Content-Length", String.valueOf(mContentLength));
mRawHeaders.set("Accept-Ranges", "bytes");
if (getHeaders().getHeaders().getStatusLine() == null)
responseCode(200);
if (mRequest.getMethod().equals(AsyncHttpHead.METHOD)) {
writeHead();
onEnd();
return;
}
Util.pump(inputStream, mContentLength, this, new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
onEnd();
}
});
}
catch (Exception e) {
responseCode(404);
end();
}
}
@Override
public void sendFile(File file) {
try {
if (mRawHeaders.get("Content-Type") == null)
mRawHeaders.set("Content-Type", AsyncHttpServer.getContentType(file.getAbsolutePath()));
FileInputStream fin = new FileInputStream(file);
sendStream(fin, file.length());
}
catch (Exception e) {
responseCode(404);
end();
}
}
@Override
public void responseCode(int code) {
String status = AsyncHttpServer.getResponseCodeDescription(code);
mRawHeaders.setStatusLine(String.format("HTTP/1.1 %d %s", code, status));
}
@Override
public void redirect(String location) {
responseCode(302);
mRawHeaders.set("Location", location);
end();
}
@Override
public void onCompleted(Exception ex) {
end();
}
@Override
public boolean isOpen() {
if (mSink != null)
return mSink.isOpen();
return mSocket.isOpen();
}
@Override
public void close() {
end();
if (mSink != null)
mSink.close();
else
mSocket.close();
}
@Override
public void setClosedCallback(CompletedCallback handler) {
mSink.setClosedCallback(handler);
}
@Override
public CompletedCallback getClosedCallback() {
return mSink.getClosedCallback();
}
@Override
public AsyncServer getServer() {
return mSocket.getServer();
}
}