/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.scripting;
import static com.github.anba.es6draft.runtime.AbstractOperations.IsCallable;
import static com.github.anba.es6draft.runtime.ExecutionContext.newScriptingExecutionContext;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.Objects;
import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import com.github.anba.es6draft.Script;
import com.github.anba.es6draft.compiler.CompilationException;
import com.github.anba.es6draft.parser.Parser;
import com.github.anba.es6draft.parser.ParserException;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.LexicalEnvironment;
import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.World;
import com.github.anba.es6draft.runtime.internal.CompatibilityOption;
import com.github.anba.es6draft.runtime.internal.Console;
import com.github.anba.es6draft.runtime.internal.RuntimeContext;
import com.github.anba.es6draft.runtime.internal.ScriptException;
import com.github.anba.es6draft.runtime.internal.ScriptLoader;
import com.github.anba.es6draft.runtime.internal.Source;
import com.github.anba.es6draft.runtime.types.Callable;
import com.github.anba.es6draft.runtime.types.ScriptObject;
/**
* Concrete implementation of the {@link AbstractScriptEngine} abstract class.
*/
final class ScriptEngineImpl extends AbstractScriptEngine implements ScriptEngine, Compilable, Invocable {
private final ScriptEngineFactoryImpl factory;
// Scripting sources have an extra scope object before the global environment record, the
// ScriptContext object. To ensure this extra scope is properly handled, we use the
// 'scripting' parser-option when evaluating the source code.
private final ScriptLoader scriptingLoader;
private final World world;
ScriptEngineImpl(ScriptEngineFactoryImpl factory) {
this.factory = factory;
/* @formatter:off */
RuntimeContext context = new RuntimeContext.Builder()
.setBaseDirectory(Paths.get("").toAbsolutePath())
.setGlobalAllocator(ScriptingGlobalObject::new)
.setConsole(new ScriptingConsole(this.context))
.setOptions(CompatibilityOption.WebCompatibility())
.build();
RuntimeContext scriptingContext = new RuntimeContext.Builder(context)
.setParserOptions(EnumSet.of(Parser.Option.Scripting))
.build();
/* @formatter:on */
this.world = new World(context);
this.scriptingLoader = new ScriptLoader(scriptingContext);
this.context.setBindings(createBindings(), ScriptContext.ENGINE_SCOPE);
}
private Realm newScriptingRealm() {
try {
return world.newInitializedRealm();
} catch (ParserException | CompilationException | IOException | URISyntaxException e) {
throw new IllegalStateException(e);
}
}
@Override
public ScriptEngineFactory getFactory() {
return factory;
}
@Override
public Bindings createBindings() {
return new GlobalBindings(newScriptingRealm());
}
@Override
public Object eval(String script, ScriptContext context) throws javax.script.ScriptException {
return eval(script(script, context), context);
}
@Override
public Object eval(Reader reader, ScriptContext context) throws javax.script.ScriptException {
return eval(script(reader, context), context);
}
@Override
public CompiledScript compile(String script) throws javax.script.ScriptException {
return new CompiledScriptImpl(this, script(script, context));
}
@Override
public CompiledScript compile(Reader reader) throws javax.script.ScriptException {
return new CompiledScriptImpl(this, script(reader, context));
}
@Override
public Object invokeFunction(String name, Object... args)
throws javax.script.ScriptException, NoSuchMethodException {
return invoke(null, Objects.requireNonNull(name), args);
}
@Override
public Object invokeMethod(Object thisValue, String name, Object... args)
throws javax.script.ScriptException, NoSuchMethodException {
if (!(thisValue instanceof ScriptObject)) {
throw new IllegalArgumentException();
}
return invoke((ScriptObject) thisValue, Objects.requireNonNull(name), args);
}
@Override
public <T> T getInterface(Class<T> clazz) {
return getInterface((ScriptObject) null, clazz);
}
@Override
public <T> T getInterface(Object thisValue, Class<T> clazz) {
if (!(thisValue instanceof ScriptObject)) {
throw new IllegalArgumentException();
}
return getInterface((ScriptObject) thisValue, clazz);
}
private Source createSource(ScriptContext context) {
String sourceName = Objects.toString(context.getAttribute(FILENAME), "<eval>");
return new Source(sourceName, 1);
}
private Script script(String sourceCode, ScriptContext context) throws javax.script.ScriptException {
Source source = createSource(context);
try {
return scriptingLoader.script(source, sourceCode);
} catch (ParserException e) {
throw new javax.script.ScriptException(e.getMessage(), e.getFile(), e.getLine(), e.getColumn());
} catch (CompilationException e) {
throw new javax.script.ScriptException(e);
}
}
private Script script(Reader reader, ScriptContext context) throws javax.script.ScriptException {
Source source = createSource(context);
try {
return scriptingLoader.script(source, reader);
} catch (ParserException e) {
throw new javax.script.ScriptException(e.getMessage(), e.getFile(), e.getLine(), e.getColumn());
} catch (CompilationException | IOException e) {
throw new javax.script.ScriptException(e);
}
}
Object eval(Script script, ScriptContext context) throws javax.script.ScriptException {
Realm realm = getEvalRealm(context);
RuntimeContext runtimeContext = realm.getWorld().getContext();
Console console = runtimeContext.getConsole();
runtimeContext.setConsole(new ScriptingConsole(context));
try {
// Prepare a new execution context before calling the generated code.
ExecutionContext evalCxt = newScriptingExecutionContext(realm, script, new LexicalEnvironment<>(
realm.getGlobalEnv(), new ScriptContextEnvironmentRecord(realm.defaultContext(), context)));
Object result = script.evaluate(evalCxt);
realm.getWorld().runEventLoop();
return TypeConverter.toJava(result);
} catch (ScriptException e) {
throw new javax.script.ScriptException(e);
} finally {
runtimeContext.setConsole(console);
}
}
private Object invoke(ScriptObject thisValue, String name, Object... args)
throws javax.script.ScriptException, NoSuchMethodException {
Realm realm = getEvalRealm(context);
RuntimeContext runtimeContext = realm.getWorld().getContext();
Console console = runtimeContext.getConsole();
runtimeContext.setConsole(new ScriptingConsole(context));
try {
Object[] arguments = TypeConverter.fromJava(args);
if (thisValue == null) {
thisValue = realm.getGlobalThis();
}
ExecutionContext cx = realm.defaultContext();
Object func = thisValue.get(cx, name, thisValue);
if (!IsCallable(func)) {
throw new NoSuchMethodException(name);
}
Object result = ((Callable) func).call(cx, thisValue, arguments);
realm.getWorld().runEventLoop();
return TypeConverter.toJava(result);
} catch (ScriptException e) {
throw new javax.script.ScriptException(e);
} finally {
runtimeContext.setConsole(console);
}
}
private <T> T getInterface(ScriptObject thisValue, Class<T> clazz) {
if (clazz == null || !clazz.isInterface()) {
throw new IllegalArgumentException();
}
Object instance = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object[] arguments = args != null ? args : new Object[] {};
return ScriptEngineImpl.this.invoke(thisValue, method.getName(), arguments);
}
});
return clazz.cast(instance);
}
private Realm getEvalRealm(ScriptContext context) {
Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
if (bindings instanceof GlobalBindings) {
// Return realm from engine scope bindings if compatible, i.e. from the same world instance.
Realm realm = ((GlobalBindings) bindings).getRealm();
if (realm.getWorld() == world) {
return realm;
}
}
// Otherwise create a new realm.
return newScriptingRealm();
}
}