/*
* Copyright 2012 Jason Miller
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jj.document;
import static jj.application.AppLocation.*;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import javax.inject.Inject;
import org.jsoup.nodes.Document;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import jj.document.servable.DocumentRequestProcessor;
import jj.engine.EngineAPI;
import jj.execution.ExecutionInstance;
import jj.execution.ExecutionLifecycleAware;
import jj.http.server.ServableResourceConfiguration;
import jj.http.server.ServableResource;
import jj.http.server.websocket.AbstractWebSocketConnectionHost;
import jj.http.server.websocket.ConnectionBroadcastStack;
import jj.http.server.websocket.CurrentWebSocketConnection;
import jj.http.server.websocket.WebSocketConnection;
import jj.http.server.websocket.WebSocketMessageProcessor;
import jj.resource.Location;
import jj.resource.ResourceThread;
import jj.resource.NoSuchResourceException;
import jj.resource.ResourceNotViableException;
import jj.script.PendingKey;
import jj.script.ScriptThread;
import jj.script.module.RootScriptEnvironment;
import jj.script.module.ScriptResource;
import jj.util.Closer;
import jj.util.SHA1Helper;
/**
* Represents a document script, and manages all of the attendant resources
* and web socket connections
*
* @author jason
*
*/
@ServableResourceConfiguration(
name = "document",
processor = DocumentScriptEnvironmentRouteProcessor.class
)
public class DocumentScriptEnvironment
extends AbstractWebSocketConnectionHost
implements ExecutionLifecycleAware, RootScriptEnvironment<Void>, ServableResource {
public static final String JJ_JS = "jj.js";
public static final String JQUERY_JS_DEV = "jquery-2.0.3.js";
public static final String JQUERY_JS = "jquery-2.0.3.min.js";
public static final String JQUERY_JS_MAP = "jquery-2.0.3.min.map";
public static final String READY_FUNCTION_KEY = "Document.ready";
// --- implementation
private final HashMap<String, Callable> functions = new HashMap<>(4);
private final String socketUri;
private final String sha1;
private final ScriptableObject scope;
private final ScriptableObject global;
private final HtmlResource html;
private final ScriptResource clientScript;
private final ScriptResource sharedScript;
private final ScriptResource serverScript;
private final WebSocketMessageProcessor processor;
private final CurrentDocumentRequestProcessor currentDocument;
private final CurrentWebSocketConnection currentConnection;
private final HashMap<PendingKey, Context<?>> contexts = new HashMap<>(10);
@Inject
DocumentScriptEnvironment(
final Dependencies dependencies,
final EngineAPI api,
final ScriptCompiler compiler,
final WebSocketMessageProcessor processor,
final CurrentDocumentRequestProcessor currentDocument,
final CurrentWebSocketConnection currentConnection
) {
super(dependencies);
html = resourceFinder.loadResource(HtmlResource.class, Public, resourceName(name()));
// NO
if (html == null) {
throw new NoSuchResourceException(getClass(), name() + "-" + resourceName(name()));
}
clientScript = resourceFinder.loadResource(ScriptResource.class, Public, ScriptResourceType.Client.suffix(name()));
sharedScript = resourceFinder.loadResource(ScriptResource.class, Public, ScriptResourceType.Shared.suffix(name()));
serverScript = resourceFinder.loadResource(ScriptResource.class, Private, ScriptResourceType.Client.suffix(name()));
sha1 = SHA1Helper.keyFor(
html.sha1(),
clientScript == null ? "none" : clientScript.sha1(),
sharedScript == null ? "none" : sharedScript.sha1(),
serverScript == null ? "none" : serverScript.sha1()
);
if (serverScript == null) {
socketUri = null;
global = null;
scope = null;
} else {
socketUri = serverPath() + ".socket";
global = api.global();
scope = configureTimers(configureModuleObjects(name(), createChainedScope(global)));
try {
compiler.compile(scope, clientScript, sharedScript, serverScript.name());
} catch (Exception e) {
throw new ResourceNotViableException(name(), e);
}
}
html.addDependent(this);
if (clientScript != null) clientScript.addDependent(this);
if (sharedScript != null) sharedScript.addDependent(this);
if (serverScript != null) serverScript.addDependent(this);
this.processor = processor;
this.currentDocument = currentDocument;
this.currentConnection = currentConnection;
}
@Override
protected String extension() {
return "html";
}
private String resourceName(final String name) {
return name + ".html";
}
@Override
public String scriptName() {
return ScriptResourceType.Client.suffix(name());
}
@Override
public String sha1() {
return sha1;
}
@Override
public boolean safeToServe() {
return true;
}
@Override
public String contentType() {
return settings.contentType();
}
@Override
public boolean compressible() {
return settings.compressible();
}
@Override
public Charset charset() {
return settings.charset();
}
@Override
public Scriptable scope() {
return scope;
}
@Override
public ScriptableObject global() {
return global;
}
@Override
public Location moduleLocation() {
return Private.and(Public);
}
@Override
public Script script() {
return serverScript == null ? null : serverScript.script();
}
public Document document() {
return html.document().clone();
}
@Override
public Callable getFunction(String name) {
return functions.get(name);
}
@Override
public void addFunction(String name, Callable function) {
functions.put(name, function);
}
@Override
public boolean removeFunction(String name) {
return functions.remove(name) != null;
}
@Override
public boolean removeFunction(String name, Callable function) {
return (functions.get(name) == function) && (functions.remove(name) == function);
}
public boolean hasServerScript() {
return serverScript != null;
}
@Override
@ResourceThread
public boolean needsReplacing() throws IOException {
// this never goes out of scope on its own
// dependency tracking handles it all
return false;
}
@Override
protected boolean removeOnReload() {
// we're a root environment! reload away, please
return false;
}
@Override
public void enteredScope() {
// nothing to do
}
@Override
public void exitedScope() {
// presumably, if there is still broadcasting to be done, then it's saved
// away with continuation state
broadcastStack = null;
}
@Override
@ScriptThread
public boolean message(WebSocketConnection connection, String message) {
return processor.process(connection, message);
}
private static class Context<T> {
final ExecutionInstance<T> source;
final T current;
final ConnectionBroadcastStack broadcastStack;
Context(final ConnectionBroadcastStack broadcastStack) {
this.source = null;
this.current = null;
this.broadcastStack = broadcastStack;
}
Context(final ExecutionInstance<T> source, T resource, final ConnectionBroadcastStack broadcastStack) {
this.source = source;
this.current = resource;
this.broadcastStack = broadcastStack;
}
public Closer enterContext() {
return source != null ? source.enterScope(current) : null;
}
}
@Override
protected void captureContextForKey(PendingKey key) {
assert !contexts.containsKey(key) : "cannot capture multiple times with the same key";
// we can't have both a document and a connection, so this works out neatly...
if (currentDocument.current() != null) {
contexts.put(key, new Context<DocumentRequestProcessor>(currentDocument, currentDocument.current(), broadcastStack));
} else if (currentConnection.trueCurrent() != null) {
contexts.put(key, new Context<WebSocketConnection>(currentConnection, currentConnection.trueCurrent(), broadcastStack));
} else {
contexts.put(key, new Context<Void>(broadcastStack));
}
}
@Override
protected Closer restoreContextForKey(PendingKey key) {
assert broadcastStack == null : "restoring into a DocumentScriptEnvironment with a standing broadcastStack";
Context<?> context = contexts.remove(key);
broadcastStack = context.broadcastStack;
Closer closer = context.enterContext();
return (closer != null) ? closer : super.restoreContextForKey(key);
}
public String socketUri() {
return socketUri;
}
public ScriptResource clientScriptResource() {
return clientScript;
}
public ScriptResource sharedScriptResource() {
return sharedScript;
}
}