package jj.document.servable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import jj.resource.ResourceTask;
import jj.script.DependsOnScriptEnvironmentInitialization;
import jj.script.ScriptTask;
import jj.script.ScriptThread;
import jj.util.Closer;
import jj.document.CurrentDocumentRequestProcessor;
import jj.document.DocumentScriptEnvironment;
import jj.execution.TaskRunner;
import jj.http.server.HttpServerRequest;
import jj.http.server.HttpServerResponse;
import jj.jjmessage.JJMessage;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import org.jsoup.nodes.Document;
import org.mozilla.javascript.Callable;
/**
* Coordinates the resources necessary to execute a request
* to serve an HTML5 Document, and hands the result back to
* to client
* @author jason
*
*/
@Singleton
public class DocumentRequestProcessor {
@SuppressWarnings("serial")
private static class FilterList extends ArrayList<DocumentFilter> {}
private final TaskRunner taskRunner;
private final DependsOnScriptEnvironmentInitialization initializer;
private final CurrentDocumentRequestProcessor currentDocument;
private final DocumentScriptEnvironment documentScriptEnvironment;
private final Document document;
private final HttpServerRequest httpRequest;
private final HttpServerResponse httpResponse;
private final Set<DocumentFilter> filters;
private ArrayList<JJMessage> messages;
@Inject
DocumentRequestProcessor(
final TaskRunner taskRunner,
final DependsOnScriptEnvironmentInitialization initializer,
final CurrentDocumentRequestProcessor currentDocument,
final DocumentScriptEnvironment dse,
final HttpServerRequest httpRequest,
final HttpServerResponse httpResponse,
// move this out!
final Set<DocumentFilter> filters
) {
this.taskRunner = taskRunner;
this.initializer = initializer;
this.currentDocument = currentDocument;
this.documentScriptEnvironment = dse;
this.document = dse.document();
this.httpRequest = httpRequest;
this.httpResponse = httpResponse;
this.filters = filters;
}
private FilterList makeFilterList(final Set<DocumentFilter> filters, final boolean io) {
FilterList filterList = new FilterList();
for (DocumentFilter filter: filters) {
if (filter.needsIO(this) && io ||
(!filter.needsIO(this) && !io)) {
filterList.add(filter);
}
}
return filterList;
}
public HttpServerRequest httpRequest() {
return httpRequest;
}
public Document document() {
return document;
}
public String baseName() {
return documentScriptEnvironment.name();
}
public void process() {
taskRunner.execute(new DocumentRequestProcessTask(documentScriptEnvironment));
}
private final class DocumentRequestProcessTask extends ScriptTask<DocumentScriptEnvironment> {
private boolean run = false;
protected DocumentRequestProcessTask(DocumentScriptEnvironment scriptEnvironment) {
super("processing document request at " + scriptEnvironment.name(), scriptEnvironment);
}
@Override
protected void begin() throws Exception {
if (!scriptEnvironment.hasServerScript()) {
respond();
} else if (scriptEnvironment.initializationDidError()) {
httpResponse.error(scriptEnvironment.initializationError());
} else if (!scriptEnvironment.initialized()) {
initializer.executeOnInitialization(scriptEnvironment, this);
} else {
run = true;
Callable readyFunction = scriptEnvironment.getFunction(DocumentScriptEnvironment.READY_FUNCTION_KEY);
if (readyFunction != null) {
try (Closer closer = currentDocument.enterScope(DocumentRequestProcessor.this)) {
// should make a request object wrapper of some sort. and perhaps response too?
pendingKey = scriptEnvironment.execute(readyFunction);
}
}
}
}
@Override
protected void complete() throws Exception {
if (run && pendingKey == null) {
respond();
}
}
@Override
protected boolean errored(Throwable cause) {
httpResponse.error(cause);
return true;
}
}
/**
* Pulls the document together and spits it out
*
* this should all be somewhere else
*/
@ScriptThread
void respond() {
try {
executeFilters(makeFilterList(filters, false));
} catch (Exception e) {
httpResponse.error(e);
}
final FilterList ioFilters = makeFilterList(filters, true);
if (ioFilters.isEmpty()) {
writeResponse();
} else {
taskRunner.execute(new ResourceTask("Document filtering requiring I/O") {
@Override
public void run() {
try {
executeFilters(ioFilters);
writeResponse();
} catch (Exception e) {
httpResponse.error(e);
}
}
});
}
}
private void executeFilters(final FilterList filterList) {
for (DocumentFilter filter : filterList) {
filter.filter(this);
}
}
private void writeResponse() {
// pretty printing is turned off because it inserts weird spaces
// into the output if there are text nodes next to element node
// and it gets REALLY ANNOYING
document.outputSettings().prettyPrint(false).indentAmount(0);
byte[] bytes = document.toString().getBytes(documentScriptEnvironment.charset());
httpResponse
.header(HttpHeaderNames.CONTENT_LENGTH, bytes.length)
// clients shouldn't cache these responses at all
.header(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_STORE)
.header(HttpHeaderNames.CONTENT_TYPE, documentScriptEnvironment.contentType())
.content(bytes)
.end();
}
@Override
public String toString() {
return getClass().getSimpleName() + ": " + documentScriptEnvironment;
}
/**
* @return
*/
DocumentScriptEnvironment documentScriptEnvironment() {
return documentScriptEnvironment;
}
/**
* adds a message intended to be processed a framework startup
* on the client.
*
* currently read in the document but needs to be moved into the connection event
*
* MOVE THIS to inside. it's so sloppy in the html
* @param message
*/
public DocumentRequestProcessor addStartupJJMessage(final JJMessage message) {
if (messages == null) {
messages = new ArrayList<>();
}
messages.add(message);
return this;
}
public List<JJMessage> startupJJMessages() {
ArrayList<JJMessage> messages = this.messages;
this.messages = null;
return messages == null ? Collections.<JJMessage>emptyList() : messages;
}
String uri() {
return httpRequest.uriMatch().uri;
}
}