package nginx.clojure;
import static nginx.clojure.MiniConstants.BYTE_ARRAY_OFFSET;
import static nginx.clojure.MiniConstants.CONTENT_TYPE;
import static nginx.clojure.MiniConstants.DEFAULT_ENCODING;
import static nginx.clojure.MiniConstants.KNOWN_RESP_HEADERS;
import static nginx.clojure.MiniConstants.NGX_DONE;
import static nginx.clojure.MiniConstants.NGX_HTTP_BODY_FILTER_PHASE;
import static nginx.clojure.MiniConstants.NGX_HTTP_CLOJURE_HEADERSO_CONTENT_TYPE_LEN_OFFSET;
import static nginx.clojure.MiniConstants.NGX_HTTP_CLOJURE_HEADERSO_CONTENT_TYPE_OFFSET;
import static nginx.clojure.MiniConstants.NGX_HTTP_CLOJURE_HEADERSO_HEADERS_OFFSET;
import static nginx.clojure.MiniConstants.NGX_HTTP_CLOJURE_HEADERSO_STATUS_OFFSET;
import static nginx.clojure.MiniConstants.NGX_HTTP_CLOJURE_REQ_HEADERS_OUT_OFFSET;
import static nginx.clojure.MiniConstants.NGX_HTTP_CLOJURE_REQ_POOL_OFFSET;
import static nginx.clojure.MiniConstants.NGX_HTTP_HEADER_FILTER_PHASE;
import static nginx.clojure.MiniConstants.NGX_HTTP_INTERNAL_SERVER_ERROR;
import static nginx.clojure.MiniConstants.NGX_HTTP_NO_CONTENT;
import static nginx.clojure.MiniConstants.NGX_HTTP_OK;
import static nginx.clojure.MiniConstants.NGX_HTTP_SWITCHING_PROTOCOLS;
import static nginx.clojure.MiniConstants.RESP_CONTENT_TYPE_HOLDER;
import static nginx.clojure.MiniConstants.STRING_CHAR_ARRAY_OFFSET;
import static nginx.clojure.NginxClojureRT.UNSAFE;
import static nginx.clojure.NginxClojureRT.coroutineEnabled;
import static nginx.clojure.NginxClojureRT.handleResponse;
import static nginx.clojure.NginxClojureRT.log;
import static nginx.clojure.NginxClojureRT.ngx_http_clojure_mem_build_file_chain;
import static nginx.clojure.NginxClojureRT.ngx_http_clojure_mem_build_temp_chain;
import static nginx.clojure.NginxClojureRT.ngx_http_clojure_mem_inc_req_count;
import static nginx.clojure.NginxClojureRT.ngx_http_set_content_type;
import static nginx.clojure.NginxClojureRT.pickByteBuffer;
import static nginx.clojure.NginxClojureRT.pushNGXInt;
import static nginx.clojure.NginxClojureRT.pushNGXSizet;
import static nginx.clojure.NginxClojureRT.pushNGXString;
import static nginx.clojure.NginxClojureRT.workers;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import nginx.clojure.Coroutine.FinishAwaredRunnable;
import nginx.clojure.NginxClojureRT.WorkerResponseContext;
import nginx.clojure.java.Constants;
import nginx.clojure.java.NginxJavaResponse;
import sun.nio.ch.DirectBuffer;
import sun.nio.cs.ThreadLocalCoders;
public abstract class NginxSimpleHandler implements NginxHandler, Configurable {
protected static ConcurrentLinkedQueue<Coroutine> pooledCoroutines = new ConcurrentLinkedQueue<Coroutine>();
protected static ConcurrentHashMap<Long, Future<WorkerResponseContext>> lastRequestEvalFutures = new ConcurrentHashMap<Long, Future<WorkerResponseContext>>();
public abstract NginxRequest makeRequest(long r, long c);
protected boolean forcePrefetchAllProperties = false;
@Override
public void config(Map<String, String> properties) {
forcePrefetchAllProperties = "true".equalsIgnoreCase(properties.get(MiniConstants.REQUEST_FORECE_PREFETCH_ALL_PROPERTIES));
}
@Override
public int execute(final long r, final long c) {
if (r == 0) { //by worker init process
NginxResponse resp = handleRequest(makeRequest(0, 0));
if (resp != null && resp.type() == NginxResponse.TYPE_FAKE_ASYNC_TAG && resp.fetchStatus(200) != 200) {
log.error("initialize error %s", resp);
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
return NGX_HTTP_OK;
}
final NginxRequest req = makeRequest(r, c);
final int phase = req.phase();
boolean isWebSocket = req.isWebSocket();
if (forcePrefetchAllProperties) {
//for safe access with another thread
req.prefetchAll();
}
if (workers == null || (isWebSocket && phase == -1)) {
if (isWebSocket) {
req.uri();
}
NginxResponse resp = handleRequest(req);
if (resp.type() == NginxResponse.TYPE_FAKE_ASYNC_TAG) {
/*
* the equivalent complete check is :
* !req.isReleased() //skip released requests
* && !( req.isHijacked() && (phase == -1 || phase == NGX_HTTP_HEADER_FILTER_PHASE)) //skips those increased hijacked requests
* && (phase == -1 || phase == NGX_HTTP_HEADER_FILTER_PHASE) //must be content handler
*/
if (!req.isReleased() && !req.isHijacked()
&& (phase == -1 || phase == NGX_HTTP_HEADER_FILTER_PHASE
|| phase == NGX_HTTP_BODY_FILTER_PHASE)) {
long oldCount = ngx_http_clojure_mem_inc_req_count(r, 1);
if (oldCount < 0) {
return (int)oldCount;
} else {
req.nativeCount(oldCount + 1);
}
}
return NGX_DONE;
}
return handleResponse(req, resp);
}
//with thread pool mode we need make it safe
if (!forcePrefetchAllProperties) {
req.prefetchAll();
}
if (phase == -1 || phase == NGX_HTTP_HEADER_FILTER_PHASE
|| phase == NGX_HTTP_BODY_FILTER_PHASE
) { // -1 means from content handler invoking
long oldCount = ngx_http_clojure_mem_inc_req_count(r, 1);
if (oldCount < 0) {
return (int)oldCount;
} else {
req.nativeCount(oldCount + 1);
}
}
final Future<WorkerResponseContext> lastFuture = lastRequestEvalFutures.get(req.nativeRequest());
Future<WorkerResponseContext> future = workers.submit(new Callable<NginxClojureRT.WorkerResponseContext>() {
@Override
public WorkerResponseContext call() throws Exception {
NginxClojureRT.getLog().debug("req %s, c %s, phase %s", req.nativeRequest(), req.nativeCount(), req.phase());
if (lastFuture != null) {
lastFuture.get();
}
NginxResponse resp = handleRequest(req);
//let output chain built before entering the main thread
return new WorkerResponseContext(resp, req);
}
});
lastRequestEvalFutures.put(req.nativeRequest(), future);
return NGX_DONE;
}
public static NginxResponse handleRequest(final NginxRequest req) {
try{
if (coroutineEnabled) {
Coroutine coroutine = pooledCoroutines.poll();
CoroutineRunner coroutineRunner;
if (coroutine == null) {
coroutineRunner = new CoroutineRunner(req);
coroutine = new Coroutine(coroutineRunner);
}else {
coroutine.reset();
coroutineRunner = (CoroutineRunner) coroutine.getProto();
coroutineRunner.request = req;
}
coroutine.resume();
if (coroutine.getState() == Coroutine.State.FINISHED) {
return coroutineRunner.response;
}else {
return new NginxJavaResponse(req, Constants.ASYNC_TAG);
}
}else {
return req.handler().process(req);
}
}catch(Throwable e){
log.error("server unhandled exception!", e);
return buildUnhandledExceptionResponse(req, e);
}
}
public static interface SimpleEntrySetter {
public Object setValue(Object value);
}
public final static SimpleEntrySetter readOnlyEntrySetter = new SimpleEntrySetter() {
public Object setValue(Object value) {
throw new UnsupportedOperationException("read only entry can not set!");
}
};
public static class SimpleEntry<K, V> implements Entry<K, V> {
public K key;
public V value;
public SimpleEntrySetter setter;
public SimpleEntry(K key, V value, SimpleEntrySetter simpleEntrySetter) {
this.key = key;
this.value = value;
this.setter = simpleEntrySetter;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
return (V)setter.setValue(value);
}
}
public static class NginxUnhandledExceptionResponse extends NginxSimpleResponse {
Throwable err;
NginxRequest r;
public NginxUnhandledExceptionResponse(NginxRequest r, Throwable e) {
this.err = e;
this.r = r;
if (r.isReleased()) {
this.type = TYPE_FATAL;
}else {
this.type = TYPE_ERROR;
}
}
@Override
public int fetchStatus(int defaultStatus) {
return 500;
}
@Override
public <K, V> Collection<Entry<K, V>> fetchHeaders() {
return (List)Arrays.asList(new SimpleEntry(CONTENT_TYPE, "text/plain", readOnlyEntrySetter));
}
@Override
public Object fetchBody() {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
err.printStackTrace(pw);
pw.close();
return sw.toString();
}
@Override
public NginxRequest request() {
return r;
}
}
public static NginxResponse buildUnhandledExceptionResponse(NginxRequest r, Throwable e) {
return new NginxUnhandledExceptionResponse(r, e);
}
public static final class CoroutineRunner implements FinishAwaredRunnable {
NginxRequest request;
NginxResponse response;
public CoroutineRunner(NginxRequest request) {
super();
this.request = request;
}
@SuppressWarnings("rawtypes")
@Override
public void run() throws SuspendExecution {
try {
response = request.handler().process(request);
}catch(Throwable e) {
response = buildUnhandledExceptionResponse(request, e);
log.error("unhandled exception in coroutine", e);
}
if (Coroutine.getActiveCoroutine().getResumeCounter() != 1) {
request.handler().completeAsyncResponse(request, response);
}
}
@Override
public void onFinished(Coroutine c) {
pooledCoroutines.add(c);
}
}
@Override
public NginxHeaderHolder fetchResponseHeaderPusher(String name) {
NginxHeaderHolder pusher = KNOWN_RESP_HEADERS.get(name);
if (pusher == null) {
pusher = new UnknownHeaderHolder(name, NGX_HTTP_CLOJURE_HEADERSO_HEADERS_OFFSET);
}
return pusher;
}
@Override
public <K, V> long prepareHeaders(NginxRequest req, long status, Collection<Map.Entry<K, V>> headers) {
long r = req.nativeRequest();
long pool = UNSAFE.getAddress(r + NGX_HTTP_CLOJURE_REQ_POOL_OFFSET);
long headers_out = r + NGX_HTTP_CLOJURE_REQ_HEADERS_OUT_OFFSET;
String contentType = null;
String server = null;
if (headers != null) {
for (Map.Entry<?, ?> hen : headers) {
Object nameObj = hen.getKey();
Object val = hen.getValue();
if (nameObj == null || val == null) {
continue;
}
String name = normalizeHeaderName(nameObj);
if (name == null || name.length() == 0) {
continue;
}
NginxHeaderHolder pusher = fetchResponseHeaderPusher(name);
if (pusher == RESP_CONTENT_TYPE_HOLDER) {
if (val instanceof String) {
contentType = (String)val;
}else { //TODO:support another types
}
}
pusher.push(headers_out, pool, val);
}
}
if (contentType == null && status != NGX_HTTP_SWITCHING_PROTOCOLS){
ngx_http_set_content_type(r);
}else {
int contentTypeLen = pushNGXString(headers_out + NGX_HTTP_CLOJURE_HEADERSO_CONTENT_TYPE_OFFSET, contentType, DEFAULT_ENCODING, pool);
//be friendly to gzip module
pushNGXSizet(headers_out + NGX_HTTP_CLOJURE_HEADERSO_CONTENT_TYPE_LEN_OFFSET, contentTypeLen);
}
pushNGXInt(headers_out + NGX_HTTP_CLOJURE_HEADERSO_STATUS_OFFSET, (int)status);
return r;
}
@Override
public long buildOutputChain(NginxResponse response) {
long r = response.request().nativeRequest();
try {
long pool = UNSAFE.getAddress(r + NGX_HTTP_CLOJURE_REQ_POOL_OFFSET);
int status = response.fetchStatus(NGX_HTTP_OK);
Object body = response.fetchBody();
long chain = defaultChainFlag(response);
if (body != null) {
chain = buildResponseItemBuf(r, body, chain);
if (chain == 0) {
return -NGX_HTTP_INTERNAL_SERVER_ERROR;
}else if (chain < 0 && chain != -204) {
return chain;
}
}else {
chain = -NGX_HTTP_NO_CONTENT;
}
if (chain == -NGX_HTTP_NO_CONTENT) {
if (response.type() == NginxResponse.TYPE_FAKE_BODY_FILTER_TAG) {
if (response.isLast()) {
chain = ngx_http_clojure_mem_build_temp_chain(r, defaultChainFlag(response), null, 0, 0);
}else {
return 0;
}
}else {
if (status == NGX_HTTP_OK) {
status = NGX_HTTP_NO_CONTENT;
}
return -status;
}
}
return chain;
}catch(Throwable e) {
log.error("server unhandled exception!", e);
return -NGX_HTTP_INTERNAL_SERVER_ERROR;
}
}
protected long defaultChainFlag(NginxResponse response) {
return 0;
}
protected long buildResponseFileBuf(File f, long r, long chain) {
ByteBuffer b = HackUtils.encode(f.getPath(), DEFAULT_ENCODING, pickByteBuffer());
if (b.remaining() < b.capacity()) {
b.array()[b.remaining()] = 0; // for file name in c language is ended with '\0'
}
chain = ngx_http_clojure_mem_build_file_chain(r, chain, b.array(), BYTE_ARRAY_OFFSET, b.remaining(), Thread.currentThread() == NginxClojureRT.NGINX_MAIN_THREAD);
if (chain <= 0) {
return chain;
}
return chain;
}
//TODO: optimize handling inputstream with large lazy data
protected long buildResponseInputStreamBuf(InputStream in, long r, final long preChain) {
try {
long chain = preChain;
long first = 0;
byte[] buf = pickByteBuffer().array();
while (true) {
int c = 0;
int pos = 0;
do {
c = in.read(buf, pos, buf.length - pos);
if (c > 0) {
pos += c;
}
}while (c >= 0 && pos < buf.length);
if (pos > 0) {
chain = ngx_http_clojure_mem_build_temp_chain(r, chain, buf, BYTE_ARRAY_OFFSET, pos);
if (chain <= 0) {
return chain;
}
if (first == 0) {
first = chain;
}
}
if (c < 0) {
break;
}
}
return preChain <= 0 ? (first == 0 ? -NGX_HTTP_NO_CONTENT : first) : chain;
}catch(IOException e) {
log.error("can not read from InputStream", e);
return -500;
}finally {
try {
in.close();
} catch (IOException e) {
log.error("can not close InputStream", e);
}
}
}
protected long buildResponseStringBuf(String s, long r, final long preChain) {
if (s == null) {
return 0;
}
if (s.length() == 0) {
return -NGX_HTTP_NO_CONTENT;
}
CharsetEncoder charsetEncoder = ThreadLocalCoders.encoderFor(DEFAULT_ENCODING)
.onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE);
ByteBuffer bb = pickByteBuffer();
CharBuffer cb = CharBuffer.wrap((char[]) UNSAFE.getObject(s, STRING_CHAR_ARRAY_OFFSET));
charsetEncoder.reset();
CoderResult result = CoderResult.UNDERFLOW;
long first = 0;
long chain = preChain;
do {
result = charsetEncoder.encode(cb, bb, true);
if (result == CoderResult.OVERFLOW) {
bb.flip();
chain = ngx_http_clojure_mem_build_temp_chain(r, chain, bb.array(), BYTE_ARRAY_OFFSET, bb.remaining());
if (chain <= 0) {
return chain;
}
bb.clear();
if (first == 0) {
first = chain;
}
} else if (result == CoderResult.UNDERFLOW) {
break;
} else {
log.error("%s can not decode string : %s", result.toString(), s);
return -NGX_HTTP_INTERNAL_SERVER_ERROR;
}
} while (true);
while (charsetEncoder.flush(bb) == CoderResult.OVERFLOW) {
bb.flip();
chain = ngx_http_clojure_mem_build_temp_chain(r, chain, bb.array(), BYTE_ARRAY_OFFSET, bb.remaining());
if (chain <= 0) {
return chain;
}
if (first == 0) {
first = chain;
}
bb.clear();
}
bb.flip();
if (bb.hasRemaining()) {
chain = ngx_http_clojure_mem_build_temp_chain(r, chain, bb.array(), BYTE_ARRAY_OFFSET, bb.remaining());
if (chain <= 0) {
return chain;
}
if (first == 0) {
first = chain;
}
bb.clear();
}
return preChain <= 0 ? first : chain ;
}
protected long buildResponseByteBufferBuf(ByteBuffer b, long r, final long preChain) {
if (b == null) {
return 0;
}
if (!b.hasRemaining()) {
return -NGX_HTTP_NO_CONTENT;
}
long chain = preChain;
if (b.isDirect()) {
chain = ngx_http_clojure_mem_build_temp_chain(r, preChain, null, ((DirectBuffer)b).address()+b.position(), b.remaining());
}else {
chain = ngx_http_clojure_mem_build_temp_chain(r, preChain, b.array(), BYTE_ARRAY_OFFSET, b.remaining());
}
b.position(b.limit());
return chain;
}
protected long buildResponseByteArrayBuf(byte[] b, long r, final long preChain) {
if (b == null) {
return 0;
}
if (b.length == 0) {
return -NGX_HTTP_NO_CONTENT;
}
return ngx_http_clojure_mem_build_temp_chain(r, preChain, b, BYTE_ARRAY_OFFSET, b.length);
}
protected long buildResponseIterableBuf(Iterable iterable, long r, long preChain) {
if (iterable == null) {
return 0;
}
Iterator i = iterable.iterator();
if (!i.hasNext()) {
return -204;
}
long chain = preChain;
long first = 0;
while (i.hasNext()) {
Object o = i.next();
if (o != null) {
long rc = buildResponseItemBuf(r, o, chain);
if (rc <= 0) {
if (rc != -NGX_HTTP_NO_CONTENT) {
return rc;
}
}else {
chain = rc;
if (first == 0) {
first = chain;
}
}
}
}
return preChain <= 0 ? (first == 0 ? -NGX_HTTP_NO_CONTENT : first) : chain;
}
protected long buildResponseItemBuf(long r, Object item, long chain) {
if (item instanceof File) {
return buildResponseFileBuf((File)item, r, chain);
}else if (item instanceof NginxChainWrappedInputStream) {
return buildNginxChainWrappedInputStreamItemBuf(r, (NginxChainWrappedInputStream)item, chain);
}else if (item instanceof InputStream) {
return buildResponseInputStreamBuf((InputStream)item, r, chain);
}else if (item instanceof String) {
return buildResponseStringBuf((String)item, r, chain);
}else if (item instanceof ByteBuffer) {
return buildResponseByteBufferBuf((ByteBuffer)item, r, chain);
}else if (item instanceof byte[]) {
return buildResponseByteArrayBuf((byte[])item, r, chain);
}
return buildResponseComplexItemBuf(r, item, chain);
}
protected long buildNginxChainWrappedInputStreamItemBuf(long r, NginxChainWrappedInputStream item, long chain) {
return item.chain;
}
protected long buildResponseComplexItemBuf(long r, Object item, long chain) {
if (item == null) {
return 0;
}
if (item instanceof Iterable) {
return buildResponseIterableBuf((Iterable)item, r, chain);
}else if (item.getClass().isArray()) {
return buildResponseIterableBuf(Arrays.asList((Object[])item), r, chain);
}
return -NGX_HTTP_INTERNAL_SERVER_ERROR;
}
protected String normalizeHeaderName(Object nameObj) {
return normalizeHeaderNameHelper(nameObj);
}
public static String normalizeHeaderNameHelper(Object nameObj) {
return nameObj instanceof String ? (String)nameObj : nameObj.toString();
}
}