package com.oracle.truffle.js.scriptengine;
import java.util.AbstractMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.script.Bindings;
import javax.script.ScriptContext;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.TypeLiteral;
import org.graalvm.polyglot.Value;
import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.MagicBindingsOptionSetter;
final class GraalJSBindings extends AbstractMap<String, Object> implements Bindings, AutoCloseable {
private static final String SCRIPT_CONTEXT_GLOBAL_BINDINGS_IMPORT_FUNCTION_NAME = "importScriptEngineGlobalBindings";
private static final TypeLiteral<Map<String, Object>> STRING_MAP = new TypeLiteral<Map<String, Object>>() {
};
private Context context;
private Map<String, Object> global;
private Value deleteProperty;
private Value clear;
private Context.Builder contextBuilder;
private ScriptContext engineScriptContext;
GraalJSBindings(Context.Builder contextBuilder, ScriptContext scriptContext) {
this.contextBuilder = contextBuilder;
this.engineScriptContext = scriptContext;
}
GraalJSBindings(Context context, ScriptContext scriptContext) {
this.context = context;
initGlobal();
this.engineScriptContext = scriptContext;
}
private void requireContext() {
if (context == null) {
initContext();
}
}
private void initContext() {
context = GraalJSScriptEngine.createDefaultContext(contextBuilder);
initGlobal();
}
private void initGlobal() {
this.global = GraalJSScriptEngine.evalInternal(context, "this").as(STRING_MAP);
}
private Value deletePropertyFunction() {
if (this.deleteProperty == null) {
this.deleteProperty = GraalJSScriptEngine.evalInternal(context, "(function(obj, prop) {delete obj[prop]})");
}
return this.deleteProperty;
}
private Value clearFunction() {
if (this.clear == null) {
this.clear = GraalJSScriptEngine.evalInternal(context, "(function(obj) {for (var prop in obj) {delete obj[prop]}})");
}
return this.clear;
}
@Override
public Object put(String name, Object v) {
checkKey(name);
if (name.startsWith(GraalJSScriptEngine.MAGIC_OPTION_PREFIX)) {
if (context == null) {
MagicBindingsOptionSetter optionSetter = GraalJSScriptEngine.MAGIC_BINDINGS_OPTION_MAP.get(name);
if (optionSetter == null) {
throw new IllegalArgumentException("unkown graal-js option \"" + name + "\"");
} else {
contextBuilder = optionSetter.setOption(contextBuilder, v);
return true;
}
} else {
throw magicOptionContextInitializedError(name);
}
}
requireContext();
return global.put(name, v);
}
@Override
public void clear() {
if (context != null) {
clearFunction().execute(global);
}
}
@Override
public Object get(Object key) {
checkKey((String) key);
requireContext();
if (engineScriptContext != null) {
importGlobalBindings(engineScriptContext);
}
return global.get(key);
}
private static void checkKey(String key) {
Objects.requireNonNull(key, "key can not be null");
if (key.isEmpty()) {
throw new IllegalArgumentException("key can not be empty");
}
}
@Override
public Object remove(Object key) {
requireContext();
Object prev = get(key);
deletePropertyFunction().execute(global, key);
return prev;
}
public Context getContext() {
requireContext();
return context;
}
@Override
public Set<Entry<String, Object>> entrySet() {
requireContext();
return global.entrySet();
}
@Override
public void close() {
if (context != null) {
context.close();
}
}
private static IllegalStateException magicOptionContextInitializedError(String name) {
return new IllegalStateException(String.format("failed to set graal-js option \"%s\": js context is already initialized", name));
}
void importGlobalBindings(ScriptContext scriptContext) {
Bindings globalBindings = scriptContext.getBindings(ScriptContext.GLOBAL_SCOPE);
if (globalBindings != null && !globalBindings.isEmpty() && this != globalBindings) {
getContext().getBindings("js").getMember(SCRIPT_CONTEXT_GLOBAL_BINDINGS_IMPORT_FUNCTION_NAME).execute(globalBindings);
}
}
void updateEngineScriptContext(ScriptContext scriptContext) {
engineScriptContext = scriptContext;
}
}