/*
* 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.script;
import static jj.script.ScriptExecutionState.*;
import static jj.server.ServerLocation.Virtual;
import java.util.HashMap;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import jj.resource.ResourceIdentifier;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.ContinuationPending;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import jj.resource.AbstractResource;
import jj.util.Closer;
/**
* <p>
* Provides basic services for {@link ScriptEnvironment}s. In particular, integrations
* into the continuation system are the main point, allowing resumable execution.
*
* <p>
* also provides methods for building rhino scopes, setting up the module loading system,
* and other small basic niceties for hooking into the system
*
* @author jason
*
*/
public abstract class AbstractScriptEnvironment<T> extends AbstractResource<T> implements ScriptEnvironment<T> {
@Singleton
protected static class AbstractScriptEnvironmentDependencies {
protected final ContinuationCoordinator continuationCoordinator;
protected final ContinuationPendingCache continuationPendingCache;
protected final Provider<PendingKey> pendingKeyProvider;
protected final RequireInnerFunction requireInnerFunction;
protected final InjectFunction injectFunction;
protected final Timers timers;
protected final Provider<RhinoContext> contextProvider;
@Inject
AbstractScriptEnvironmentDependencies(
final ContinuationCoordinator continuationCoordinator,
final ContinuationPendingCache continuationPendingCache,
final Provider<PendingKey> pendingKeyProvider,
final RequireInnerFunction requireInnerFunction,
final InjectFunction injectFunction,
final Timers timers,
final Provider<RhinoContext> contextProvider
) {
this.continuationCoordinator = continuationCoordinator;
this.continuationPendingCache = continuationPendingCache;
this.pendingKeyProvider = pendingKeyProvider;
this.requireInnerFunction = requireInnerFunction;
this.injectFunction = injectFunction;
this.timers = timers;
this.contextProvider = contextProvider;
}
}
// bundles up the dependencies for this object, so that descendents don't need to
// to be updated when this changes, cause it just might change more!
// package-private access on the fields for testing
public static class Dependencies extends AbstractResource.Dependencies {
protected final AbstractScriptEnvironmentDependencies scriptEnvironmentDependencies;
@Inject
protected Dependencies(
final AbstractResourceDependencies abstractResourceDependencies,
final AbstractScriptEnvironmentDependencies abstractScriptEnvironmentDependencies,
final ResourceIdentifier<?, ?> identifier
) {
super(abstractResourceDependencies, identifier);
this.scriptEnvironmentDependencies = abstractScriptEnvironmentDependencies;
}
}
/**
* convenience to get to a RhinoContext for script execution
*/
protected final Provider<RhinoContext> contextProvider;
private final HashMap<PendingKey, ContinuationPending> continuationPendings = new HashMap<>();
private final ContinuationCoordinator continuationCoordinator;
private final ContinuationPendingCache continuationPendingCache;
private final Dependencies dependencies;
private volatile ScriptExecutionState state = Unitialized;
private volatile Throwable initializationError;
protected AbstractScriptEnvironment(Dependencies dependencies) {
super(dependencies);
this.contextProvider = dependencies.scriptEnvironmentDependencies.contextProvider;
this.continuationPendingCache = dependencies.scriptEnvironmentDependencies.continuationPendingCache;
this.continuationCoordinator = dependencies.scriptEnvironmentDependencies.continuationCoordinator;
this.dependencies = dependencies;
}
@Override
public ScriptableObject newObject() {
try (RhinoContext context = contextProvider.get()) {
return context.newObject(scope());
}
}
@Override
public boolean initialized() {
return state == Initialized;
}
@Override
public boolean initializing() {
return state == Initializing;
}
@Override
public Throwable initializationError() {
return initializationError;
}
@Override
public boolean initializationDidError() {
return state == Errored;
}
@Override
public PendingKey execute(Script script) {
return continuationCoordinator.execute(this, script);
}
@Override
public PendingKey execute(Callable callable, Object...args) {
return continuationCoordinator.execute(this, callable, args);
}
/**
* Resume a continuation in this environment
*/
PendingKey resumeContinuation(PendingKey pendingKey, Object result) {
return continuationCoordinator.resumeContinuation(this, pendingKey, result);
}
/**
* Await a continuation in this environment
*/
<S extends ScriptEnvironment<?>> void awaitContinuation(ScriptTask<S> task) {
continuationPendingCache.storeForContinuation(task);
}
PendingKey beginInitializing() {
assert state == Unitialized : "wrong state to initialize";
state = Initializing;
return doInitialize();
}
/**
* mark this environment as being initialized
*/
void initialized(boolean initialized) {
if (initialized) {
state = Initialized;
}
}
/**
* mark this environment as having experienced an initialization error
*/
void initializationError(Throwable cause) {
state = Errored;
this.initializationError = cause;
}
/**
* Override this function for custom initialization functionality
*/
protected PendingKey doInitialize() {
return script() == null ? null : execute(script());
}
@Override
protected void died() {
// mark dead
state = Dead;
// when a script environment dies, we can dump any pending tasks on the floor
dependencies.scriptEnvironmentDependencies.continuationPendingCache.removePendingTasks(continuationPendings.keySet());
// and publish it to the world
publisher.publish(new ScriptEnvironmentDied(this));
}
/**
* prepare this environment for a continuation
* @return the key to resume the continuation, with a fully saved context
*/
PendingKey createContinuationContext(final ContinuationPending continuationPending) {
PendingKey key = dependencies.scriptEnvironmentDependencies.pendingKeyProvider.get();
//noinspection ThrowableResultOfMethodCallIgnored
continuationPendings.put(key, continuationPending);
captureContextForKey(key);
return key;
}
/**
* @return the captured execution state for a given key
*/
ContinuationPending continuationPending(final PendingKey key) {
assert continuationPendings.containsKey(key) : "trying to retrieve a nonexistent continuation for " + key;
return continuationPendings.remove(key);
}
/**
* Implement to perform environment-specific context capture for a continuation, associated to the given key
*/
protected void captureContextForKey(PendingKey key) {
// nothing to do in the abstract, but specific type will have things
// DocumentScriptEnvironment needs to save connections and documents, for example
}
/**
* restore an environment-specific context for the continuation associated to the given key.
* @return a {@link Closer} to clean up any restored context when the
*/
protected Closer restoreContextForKey(PendingKey key) {
return () -> { /* nothing to do */ };
}
@Override
public Object exports() {
try (RhinoContext context = contextProvider.get()) {
return scope() == null ? Undefined.instance : context.evaluateString(scope(), "module.exports", "evaluating exports");
}
}
/**
* @return a pendingKey if the completion of the initialization task should resume something
* or null if nothing. the abstract returns null
*/
protected PendingKey initializationContinuationPendingKey() {
return null;
}
/**
* <p>
* creates a child scope for the given parent. lookups will delegate to the parent if a given
* reference is not found in the child, but all creation will occur in the child. the parent is typically
* sealed and cannot be impacted in any case
*
* <p>
* Generally, you want to inject the {@link Global} scope, and use that as a parent. it has all the
* standard javascript objects initialized, but it is sealed and intended to be shared server-wide
*
* @return the new child scope
*/
protected ScriptableObject createChainedScope(final ScriptableObject parent) {
try (RhinoContext context = contextProvider.get()) {
return context.newChainedScope(parent);
}
}
/**
* <p>
* creates the usual timer functions in the supplied scope.
*
* <p>
* functions are setInterval, setTimeout, clearInterval, and clearTimeout. they behave much like
* their browser progenitors
*
* @return the supplied scope
*/
protected ScriptableObject configureTimers(final ScriptableObject localScope) {
assert !localScope.isSealed() : "cannot configure timers on a sealed scope";
localScope.defineProperty("setInterval", dependencies.scriptEnvironmentDependencies.timers.setInterval, ScriptableObject.EMPTY);
localScope.defineProperty("setTimeout", dependencies.scriptEnvironmentDependencies.timers.setTimeout, ScriptableObject.EMPTY);
localScope.defineProperty("clearInterval", dependencies.scriptEnvironmentDependencies.timers.clearInterval, ScriptableObject.EMPTY);
localScope.defineProperty("clearTimeout", dependencies.scriptEnvironmentDependencies.timers.clearTimeout, ScriptableObject.EMPTY);
return localScope;
}
/**
* Installs the {@link InjectFunction} under the standard name defined in {@link InjectFunction#NAME}
* into the supplied scope.
*
* @return the supplied scope
*/
protected ScriptableObject configureInjectFunction(final ScriptableObject localScope) {
return configureInjectFunction(localScope, InjectFunction.NAME);
}
/**
* Installs the {@link InjectFunction} under the supplied name into the supplied scope.
*
* @return the supplied scope
*/
protected ScriptableObject configureInjectFunction(final ScriptableObject localScope, final String name) {
assert !localScope.isSealed() : "cannot configure inject function on a sealed scope";
localScope.defineProperty(name, dependencies.scriptEnvironmentDependencies.injectFunction, ScriptableObject.CONST);
return localScope;
}
/**
* Creates the CommonJS module set-up in the supplied scope, and adds a require function under the name
* "require"
*
* @return the supplied scope
*/
protected ScriptableObject configureModuleObjects(
final String moduleIdentifier,
final ScriptableObject localScope
) {
return configureModuleObjects(moduleIdentifier, localScope, "require");
}
/**
* Configures top-level module objects in a given scope, including a require function and assignable exports
* that are made available via {@link #exports()}. the require function is installed under the supplied name
*/
protected ScriptableObject configureModuleObjects(
final String moduleIdentifier,
final ScriptableObject localScope,
final String requireFunctionName
) {
assert !localScope.isSealed() : "cannot configure module objects on a sealed scope";
try (RhinoContext context = contextProvider.get()) {
// setting up the 'module' property as described in
// the commonjs module 1.1.1 specification
// in the case of the top-level server script, the id
// will be the name, which fortunately happens to be
// exactly what is required
ScriptableObject module = context.newObject(localScope);
ScriptableObject exports = context.newObject(localScope);
module.defineProperty("id", moduleIdentifier, ScriptableObject.CONST);
module.defineProperty("exports", exports, ScriptableObject.EMPTY);
module.defineProperty("requireInner", dependencies.scriptEnvironmentDependencies.requireInnerFunction, ScriptableObject.EMPTY);
localScope.defineProperty("module", module, ScriptableObject.CONST);
localScope.defineProperty("exports", exports, ScriptableObject.CONST);
// define the require method and the exports object here as well.
// follow the node.js concept of module.exports === exports, and
// assigning to module.exports changes the exports object,
// potentially to a function
// the ability to load this as a ScriptResource is implicit, i believe
ScriptableObject require = (ScriptableObject)context.evaluateString(
localScope,
"(function(module) {\n" +
"var idFormat = /^(?:\\.?\\/)?[a-zA-Z][\\/\\w-]*$/;\n" +
"var requireInner = module.requireInner;\n" +
"return function(id) {\n" +
"if (!id || typeof id != 'string' || !idFormat.test(id)) {\n" +
"throw new Error(id + ' is not a valid module identifier');\n" +
"}\n" +
"var result = requireInner(id, module.id);\n" +
"if (result === null || result === false) {\n" +
"throw new Error('module \"' + id + '\" cannot be found');\n" +
"}\n" +
"return result;\n" +
"}\n" +
"})(module);",
AbstractScriptEnvironment.class.getSimpleName() + " require function definition"
);
localScope.defineProperty(requireFunctionName, require, ScriptableObject.CONST);
ScriptableObject.deleteProperty(module, "requireInner");
}
return localScope;
}
@Override
public String toString() {
return super.toString() + " {state=" + state + "}";
}
}