package com.koushikdutta.async.http.server;
import android.text.TextUtils;
import com.koushikdutta.async.AsyncServer;
import com.koushikdutta.async.AsyncSocket;
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.DataCallback;
import com.koushikdutta.async.callback.WritableCallback;
import com.koushikdutta.async.http.AsyncHttpHead;
import com.koushikdutta.async.http.AsyncHttpResponse;
import com.koushikdutta.async.http.Headers;
import com.koushikdutta.async.http.HttpUtil;
import com.koushikdutta.async.http.Protocol;
import com.koushikdutta.async.http.filter.ChunkedOutputFilter;
import com.koushikdutta.async.util.StreamUtility;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Locale;
public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse {
private Headers mRawHeaders = new Headers();
private long mContentLength = -1;
@Override
public Headers getHeaders() {
return mRawHeaders;
}
public AsyncSocket getSocket() {
return mSocket;
}
AsyncSocket mSocket;
AsyncHttpServerRequestImpl mRequest;
AsyncHttpServerResponseImpl(AsyncSocket socket, AsyncHttpServerRequestImpl req) {
mSocket = socket;
mRequest = req;
if (HttpUtil.isKeepAlive(Protocol.HTTP_1_1, req.getHeaders()))
mRawHeaders.set("Connection", "Keep-Alive");
}
@Override
public void write(ByteBufferList bb) {
// order is important here...
assert !mEnded;
// do the header write... this will call onWritable, which may be reentrant
if (!headWritten)
initFirstWrite();
// now check to see if the list is empty. reentrancy may cause it to empty itself.
if (bb.remaining() == 0)
return;
// null sink means that the header has not finished writing
if (mSink == null)
return;
// can successfully write!
mSink.write(bb);
}
boolean headWritten = false;
DataSink mSink;
void initFirstWrite() {
if (headWritten)
return;
headWritten = true;
final boolean isChunked;
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 = Long.valueOf(contentLength);
}
if (mContentLength < 0 && canUseChunked) {
mRawHeaders.set("Transfer-Encoding", "Chunked");
isChunked = true;
}
else {
isChunked = false;
}
String statusLine = String.format(Locale.ENGLISH, "HTTP/1.1 %s %s", code, AsyncHttpServer.getResponseCodeDescription(code));
String rh = mRawHeaders.toPrefixString(statusLine);
Util.writeAll(mSocket, rh.getBytes(), new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
if (ex != null) {
report(ex);
return;
}
if (isChunked) {
ChunkedOutputFilter chunked = new ChunkedOutputFilter(mSocket);
chunked.setMaxBuffer(0);
mSink = chunked;
}
else {
mSink = mSocket;
}
mSink.setClosedCallback(closedCallback);
closedCallback = null;
mSink.setWriteableCallback(writable);
writable = null;
if (ended) {
// the response ended while headers were written
end();
return;
}
getServer().post(new Runnable() {
@Override
public void run() {
WritableCallback wb = getWriteableCallback();
if (wb != null)
wb.onWriteable();
}
});
}
});
}
WritableCallback writable;
@Override
public void setWriteableCallback(WritableCallback handler) {
if (mSink != null)
mSink.setWriteableCallback(handler);
else
writable = handler;
}
@Override
public WritableCallback getWriteableCallback() {
if (mSink != null)
return mSink.getWriteableCallback();
return writable;
}
boolean ended;
@Override
public void end() {
if (ended)
return;
ended = true;
if (headWritten && mSink == null) {
// header is in the process of being written... bail out.
// end will be called again after finished.
return;
}
if (!headWritten) {
// end was called, and no head or body was yet written,
// so strip the transfer encoding as that is superfluous.
mRawHeaders.remove("Transfer-Encoding");
}
if (mSink instanceof ChunkedOutputFilter) {
((ChunkedOutputFilter)mSink).setMaxBuffer(Integer.MAX_VALUE);
mSink.write(new ByteBufferList());
onEnd();
}
else if (!headWritten) {
if (!mRequest.getMethod().equalsIgnoreCase(AsyncHttpHead.METHOD))
send("text/html", "");
else {
writeHead();
onEnd();
}
}
else {
onEnd();
}
}
@Override
public void writeHead() {
initFirstWrite();
}
@Override
public void setContentType(String contentType) {
mRawHeaders.set("Content-Type", contentType);
}
@Override
public void send(String contentType, byte[] bytes) {
assert mContentLength < 0;
mContentLength = bytes.length;
mRawHeaders.set("Content-Length", Integer.toString(bytes.length));
mRawHeaders.set("Content-Type", contentType);
Util.writeAll(this, bytes, new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
onEnd();
}
});
}
@Override
public void send(String contentType, final String string) {
try {
send(contentType, string.getBytes("UTF-8"));
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
boolean mEnded;
protected void onEnd() {
mEnded = true;
}
protected void report(Exception e) {
}
@Override
public void send(String string) {
String contentType = mRawHeaders.get("Content-Type");
if (contentType == null)
contentType = "text/html; charset=utf-8";
send(contentType, string);
}
@Override
public void send(JSONObject json) {
send("application/json; charset=utf-8", json.toString());
}
@Override
public void sendStream(final InputStream inputStream, long totalLength) {
long start = 0;
long end = totalLength - 1;
String range = mRequest.getHeaders().get("Range");
if (range != null) {
String[] parts = range.split("=");
if (parts.length != 2 || !"bytes".equals(parts[0])) {
// Requested range not satisfiable
code(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;
code(206);
getHeaders().set("Content-Range", String.format(Locale.ENGLISH, "bytes %d-%d/%d", start, end, totalLength));
}
catch (Exception e) {
code(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 (mRequest.getMethod().equals(AsyncHttpHead.METHOD)) {
writeHead();
onEnd();
return;
}
Util.pump(inputStream, mContentLength, this, new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
StreamUtility.closeQuietly(inputStream);
onEnd();
}
});
}
catch (Exception e) {
code(500);
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(new BufferedInputStream(fin, 64000), file.length());
}
catch (FileNotFoundException e) {
code(404);
end();
}
}
@Override
public void proxy(final AsyncHttpResponse remoteResponse) {
code(remoteResponse.code());
remoteResponse.headers().removeAll("Transfer-Encoding");
remoteResponse.headers().removeAll("Content-Encoding");
remoteResponse.headers().removeAll("Connection");
getHeaders().addAll(remoteResponse.headers());
// TODO: remove?
remoteResponse.headers().set("Connection", "close");
Util.pump(remoteResponse, this, new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
remoteResponse.setEndCallback(new NullCompletedCallback());
remoteResponse.setDataCallback(new DataCallback.NullDataCallback());
end();
}
});
}
int code = 200;
@Override
public AsyncHttpServerResponse code(int code) {
this.code = code;
return this;
}
@Override
public int code() {
return code;
}
@Override
public void redirect(String location) {
code(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();
}
CompletedCallback closedCallback;
@Override
public void setClosedCallback(CompletedCallback handler) {
if (mSink != null)
mSink.setClosedCallback(handler);
else
closedCallback = handler;
}
@Override
public CompletedCallback getClosedCallback() {
if (mSink != null)
return mSink.getClosedCallback();
return closedCallback;
}
@Override
public AsyncServer getServer() {
return mSocket.getServer();
}
@Override
public String toString() {
if (mRawHeaders == null)
return super.toString();
String statusLine = String.format(Locale.ENGLISH, "HTTP/1.1 %s %s", code, AsyncHttpServer.getResponseCodeDescription(code));
return mRawHeaders.toPrefixString(statusLine);
}
}