843 lines
34 KiB
Java
843 lines
34 KiB
Java
/*
|
|
* Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* The Universal Permissive License (UPL), Version 1.0
|
|
*
|
|
* Subject to the condition set forth below, permission is hereby granted to any
|
|
* person obtaining a copy of this software, associated documentation and/or
|
|
* data (collectively the "Software"), free of charge and under any and all
|
|
* copyright rights in the Software, and any and all patent rights owned or
|
|
* freely licensable by each licensor hereunder covering either (i) the
|
|
* unmodified Software as contributed to or provided by such licensor, or (ii)
|
|
* the Larger Works (as defined below), to deal in both
|
|
*
|
|
* (a) the Software, and
|
|
*
|
|
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
|
|
* one is included with the Software each a "Larger Work" to which the Software
|
|
* is contributed by such licensors),
|
|
*
|
|
* without restriction, including without limitation the rights to copy, create
|
|
* derivative works of, display, perform, and distribute the Software and make,
|
|
* use, sell, offer for sale, import, export, have made, and have sold the
|
|
* Software and the Larger Work(s), and to sublicense the foregoing rights on
|
|
* either these or other terms.
|
|
*
|
|
* This license is subject to the following condition:
|
|
*
|
|
* The above copyright notice and either this complete permission notice or at a
|
|
* minimum a reference to the UPL must be included in all copies or substantial
|
|
* portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
package jmri.script.jsr223graalpython;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.Reader;
|
|
import java.io.Writer;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.CharBuffer;
|
|
import java.nio.charset.CharsetDecoder;
|
|
import java.nio.charset.CharsetEncoder;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.function.Predicate;
|
|
|
|
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.ScriptException;
|
|
|
|
import org.graalvm.collections.EconomicMap;
|
|
import org.graalvm.collections.EconomicSet;
|
|
import org.graalvm.polyglot.Context;
|
|
import org.graalvm.polyglot.Context.Builder;
|
|
import org.graalvm.polyglot.Engine;
|
|
import org.graalvm.polyglot.HostAccess;
|
|
import org.graalvm.polyglot.HostAccess.TargetMappingPrecedence;
|
|
import org.graalvm.polyglot.PolyglotException;
|
|
import org.graalvm.polyglot.Source;
|
|
import org.graalvm.polyglot.Value;
|
|
import org.graalvm.polyglot.proxy.Proxy;
|
|
|
|
/**
|
|
* A Graal.JS implementation of the script engine. It provides access to the polyglot context using
|
|
* {@link #getPolyglotContext()}.
|
|
*/
|
|
public final class GraalJSScriptEngine extends AbstractScriptEngine implements Compilable, Invocable, AutoCloseable {
|
|
|
|
private static final String ID = "python";
|
|
private static final String POLYGLOT_CONTEXT = "polyglot.context";
|
|
private static final String OUT_SYMBOL = "$$internal.out$$";
|
|
private static final String IN_SYMBOL = "$$internal.in$$";
|
|
private static final String ERR_SYMBOL = "$$internal.err$$";
|
|
private static final String JS_SYNTAX_EXTENSIONS_OPTION = "js.syntax-extensions";
|
|
private static final String JS_SCRIPT_ENGINE_GLOBAL_SCOPE_IMPORT_OPTION = "js.script-engine-global-scope-import";
|
|
private static final String JS_LOAD_OPTION = "js.load";
|
|
private static final String JS_PRINT_OPTION = "js.print";
|
|
private static final String JS_GLOBAL_ARGUMENTS_OPTION = "js.global-arguments";
|
|
private static final String NASHORN_COMPATIBILITY_MODE_SYSTEM_PROPERTY = "polyglot.js.nashorn-compat";
|
|
private static final String INSECURE_SCRIPTENGINE_ACCESS_SYSTEM_PROPERTY = "graaljs.insecure-scriptengine-access";
|
|
static final String MAGIC_OPTION_PREFIX = "polyglot.js.";
|
|
|
|
private static final HostAccess NASHORN_HOST_ACCESS = createNashornHostAccess();
|
|
|
|
private static HostAccess createNashornHostAccess() {
|
|
HostAccess.Builder b = HostAccess.newBuilder(HostAccess.ALL);
|
|
// Last resort conversions similar to those in NashornBottomLinker.
|
|
b.targetTypeMapping(Value.class, String.class, v -> !v.isNull(), v -> toString(v), TargetMappingPrecedence.LOWEST);
|
|
b.targetTypeMapping(Number.class, Integer.class, n -> true, n -> n.intValue(), TargetMappingPrecedence.LOWEST);
|
|
b.targetTypeMapping(Number.class, Double.class, n -> true, n -> n.doubleValue(), TargetMappingPrecedence.LOWEST);
|
|
b.targetTypeMapping(Number.class, Long.class, n -> true, n -> n.longValue(), TargetMappingPrecedence.LOWEST);
|
|
b.targetTypeMapping(Number.class, Boolean.class, n -> true, n -> toBoolean(n.doubleValue()), TargetMappingPrecedence.LOWEST);
|
|
b.targetTypeMapping(String.class, Boolean.class, n -> true, n -> !n.isEmpty(), TargetMappingPrecedence.LOWEST);
|
|
return b.build();
|
|
}
|
|
|
|
// ToString() operation
|
|
private static String toString(Value value) {
|
|
return toPrimitive(value).toString();
|
|
}
|
|
|
|
// "Type(result) is not Object" heuristic for the purpose of ToPrimitive() conversion
|
|
private static boolean isPrimitive(Value value) {
|
|
return value.isString() || value.isNumber() || value.isBoolean() || value.isNull();
|
|
}
|
|
|
|
// ToPrimitive()/OrdinaryToPrimitive() operation
|
|
private static Value toPrimitive(Value value) {
|
|
if (value.hasMembers()) {
|
|
for (String methodName : new String[]{"toString", "valueOf"}) {
|
|
if (value.canInvokeMember(methodName)) {
|
|
Value maybePrimitive = value.invokeMember(methodName);
|
|
if (isPrimitive(maybePrimitive)) {
|
|
return maybePrimitive;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (isPrimitive(value)) {
|
|
return value;
|
|
} else {
|
|
throw new ClassCastException();
|
|
}
|
|
}
|
|
|
|
private static boolean toBoolean(double d) {
|
|
return d != 0.0 && !Double.isNaN(d);
|
|
}
|
|
|
|
interface MagicBindingsOptionSetter {
|
|
|
|
String getOptionKey();
|
|
|
|
Context.Builder setOption(Builder builder, Object value);
|
|
}
|
|
|
|
private static boolean toBoolean(MagicBindingsOptionSetter optionSetter, Object value) {
|
|
if (!(value instanceof Boolean)) {
|
|
throw magicOptionValueErrorBool(optionSetter.getOptionKey(), value);
|
|
}
|
|
return (Boolean) value;
|
|
}
|
|
|
|
private static final MagicBindingsOptionSetter[] MAGIC_OPTION_SETTERS = new MagicBindingsOptionSetter[]{new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "allowHostAccess";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
return builder.allowHostAccess(toBoolean(this, value) ? HostAccess.ALL : HostAccess.NONE);
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "allowNativeAccess";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
return builder.allowNativeAccess(toBoolean(this, value));
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "allowCreateThread";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
return builder.allowCreateThread(toBoolean(this, value));
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "allowIO";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
return builder.allowIO(toBoolean(this, value));
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "allowHostClassLookup";
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
if (value instanceof Boolean) {
|
|
boolean enabled = (Boolean) value;
|
|
return builder.allowHostClassLookup(enabled ? s -> true : null);
|
|
} else {
|
|
try {
|
|
return builder.allowHostClassLookup((Predicate<String>) value);
|
|
} catch (ClassCastException e) {
|
|
throw new IllegalArgumentException(String.format("failed to set graal-js option \"%s\": expected a boolean or Predicate<String> value, got \"%s\"", getOptionKey(), value));
|
|
}
|
|
}
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "allowHostClassLoading";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
return builder.allowHostClassLoading(toBoolean(this, value));
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "allowAllAccess";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
return builder.allowAllAccess(toBoolean(this, value));
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "nashorn-compat";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
boolean val = toBoolean(this, value);
|
|
if (val) {
|
|
updateForNashornCompatibilityMode(builder);
|
|
}
|
|
return builder.option("js.nashorn-compat", String.valueOf(val));
|
|
}
|
|
}, new MagicBindingsOptionSetter() {
|
|
|
|
@Override
|
|
public String getOptionKey() {
|
|
return MAGIC_OPTION_PREFIX + "ecmascript-version";
|
|
}
|
|
|
|
@Override
|
|
public Builder setOption(Builder builder, Object value) {
|
|
return builder.option("js.ecmascript-version", String.valueOf(value));
|
|
}
|
|
}};
|
|
|
|
private static final EconomicSet<String> MAGIC_BINDINGS_OPTION_KEYS = EconomicSet.create();
|
|
static final EconomicMap<String, MagicBindingsOptionSetter> MAGIC_BINDINGS_OPTION_MAP = EconomicMap.create();
|
|
private static final boolean NASHORN_COMPATIBILITY_MODE = true; // wasBoolean.getBoolean(NASHORN_COMPATIBILITY_MODE_SYSTEM_PROPERTY);
|
|
|
|
static {
|
|
for (MagicBindingsOptionSetter setter : MAGIC_OPTION_SETTERS) {
|
|
MAGIC_BINDINGS_OPTION_KEYS.add(setter.getOptionKey());
|
|
MAGIC_BINDINGS_OPTION_MAP.put(setter.getOptionKey(), setter);
|
|
}
|
|
}
|
|
|
|
private final GraalJSEngineFactory factory;
|
|
private final Context.Builder contextConfig;
|
|
|
|
private boolean evalCalled;
|
|
|
|
public GraalJSScriptEngine(GraalJSEngineFactory factory) {
|
|
this(factory, factory.getPolyglotEngine(), null);
|
|
log.debug("ctor(Factory) invoked");
|
|
}
|
|
|
|
GraalJSScriptEngine(GraalJSEngineFactory factory, Engine engine, Context.Builder contextConfig) {
|
|
log.debug("ctor invoked");
|
|
Engine engineToUse = engine;
|
|
if (engineToUse == null) {
|
|
engineToUse = Engine.newBuilder().allowExperimentalOptions(true).build();
|
|
}
|
|
Context.Builder contextConfigToUse = contextConfig;
|
|
if (contextConfigToUse == null) {
|
|
// default config
|
|
contextConfigToUse = Context.newBuilder(ID).allowExperimentalOptions(true);
|
|
contextConfigToUse.option(JS_SYNTAX_EXTENSIONS_OPTION, "true");
|
|
contextConfigToUse.option(JS_LOAD_OPTION, "true");
|
|
contextConfigToUse.option(JS_PRINT_OPTION, "true");
|
|
contextConfigToUse.option(JS_GLOBAL_ARGUMENTS_OPTION, "true");
|
|
if (NASHORN_COMPATIBILITY_MODE) {
|
|
updateForNashornCompatibilityMode(contextConfigToUse);
|
|
} else if (Boolean.getBoolean(INSECURE_SCRIPTENGINE_ACCESS_SYSTEM_PROPERTY)) {
|
|
updateForScriptEngineAccessibility(contextConfigToUse);
|
|
}
|
|
}
|
|
this.factory = (factory == null) ? new GraalJSEngineFactory(engineToUse) : factory;
|
|
this.contextConfig = contextConfigToUse.option(JS_SCRIPT_ENGINE_GLOBAL_SCOPE_IMPORT_OPTION, "true").engine(engineToUse);
|
|
this.context.setBindings(new GraalJSBindings(this.contextConfig, this.context), ScriptContext.ENGINE_SCOPE);
|
|
}
|
|
|
|
private static void updateForNashornCompatibilityMode(Context.Builder builder) {
|
|
builder.allowAllAccess(true);
|
|
builder.allowHostAccess(NASHORN_HOST_ACCESS);
|
|
// builder.useSystemExit(true);
|
|
}
|
|
|
|
private static void updateForScriptEngineAccessibility(Context.Builder builder) {
|
|
builder.allowHostAccess(HostAccess.ALL);
|
|
}
|
|
|
|
static Context createDefaultContext(Context.Builder builder) {
|
|
DelegatingInputStream in = new DelegatingInputStream();
|
|
DelegatingOutputStream out = new DelegatingOutputStream();
|
|
DelegatingOutputStream err = new DelegatingOutputStream();
|
|
builder
|
|
.allowAllAccess(true)
|
|
.in(in).out(out).err(err);
|
|
Context ctx = builder.build();
|
|
ctx.getPolyglotBindings().putMember(OUT_SYMBOL, out);
|
|
ctx.getPolyglotBindings().putMember(ERR_SYMBOL, err);
|
|
ctx.getPolyglotBindings().putMember(IN_SYMBOL, in);
|
|
return ctx;
|
|
}
|
|
|
|
/**
|
|
* Closes the current context and makes it unusable. Operations performed after closing will
|
|
* throw an {@link IllegalStateException}.
|
|
*/
|
|
@Override
|
|
public void close() {
|
|
log.debug("close invoked");
|
|
getPolyglotContext().close();
|
|
}
|
|
|
|
/**
|
|
* Returns the polyglot engine associated with this script engine.
|
|
* @return the polyglot engine associated with this script engine.
|
|
*/
|
|
public Engine getPolyglotEngine() {
|
|
log.debug("getPolyglotEngine invoked");
|
|
return factory.getPolyglotEngine();
|
|
}
|
|
|
|
/**
|
|
* Returns the polyglot context associated with the default ScriptContext of the engine.
|
|
*
|
|
* @see #getPolyglotContext(ScriptContext) to access the polyglot context of a particular
|
|
* context.
|
|
*
|
|
* @return polyglot Context from the `context` member variable
|
|
*/
|
|
public Context getPolyglotContext() {
|
|
log.debug("getPolyglotContext invoked");
|
|
return getPolyglotContext(context);
|
|
}
|
|
|
|
/**
|
|
* Returns the polyglot context associated with a ScriptContext. If the context is not yet
|
|
* initialized then it will be initialized using the default context builder specified in
|
|
* {@link #create(Engine, org.graalvm.polyglot.Context.Builder)}.
|
|
* @param ctxt Input to the creation
|
|
* @return a Context from the ScriptContext
|
|
*/
|
|
public Context getPolyglotContext(ScriptContext ctxt) {
|
|
log.trace("getPolyglotContext with {}", ctxt);
|
|
return getOrCreateGraalJSBindings(ctxt).getContext();
|
|
}
|
|
|
|
static Value evalInternal(Context context, String script) {
|
|
log.trace("evalInternal with ID {} \"{}\", \"{}\"", ID, context, script);
|
|
try {
|
|
return context.eval(Source.newBuilder(ID, script, "internal-script").internal(true).buildLiteral());
|
|
} catch (Exception e) {
|
|
log.warn("exception in evalInternal", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Bindings createBindings() {
|
|
log.debug("createBindings");
|
|
return new GraalJSBindings(contextConfig, null);
|
|
}
|
|
|
|
@Override
|
|
public void setBindings(Bindings bindings, int scope) {
|
|
log.debug("setBindings {} scope: {}", bindings, scope);
|
|
if (scope == ScriptContext.ENGINE_SCOPE) {
|
|
Bindings oldBindings = getBindings(scope);
|
|
if (oldBindings instanceof GraalJSBindings) {
|
|
((GraalJSBindings) oldBindings).updateEngineScriptContext(null);
|
|
}
|
|
}
|
|
super.setBindings(bindings, scope);
|
|
if (scope == ScriptContext.ENGINE_SCOPE && (bindings instanceof GraalJSBindings)) {
|
|
((GraalJSBindings) bindings).updateEngineScriptContext(getContext());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Object eval(Reader reader, ScriptContext ctxt) throws ScriptException {
|
|
return eval(createSource(read(reader), ctxt), ctxt);
|
|
}
|
|
|
|
static String read(Reader reader) throws ScriptException {
|
|
final StringBuilder builder = new StringBuilder();
|
|
final char[] buffer = new char[1024];
|
|
try {
|
|
try {
|
|
while (true) {
|
|
final int count = reader.read(buffer);
|
|
if (count == -1) {
|
|
break;
|
|
}
|
|
builder.append(buffer, 0, count);
|
|
}
|
|
} finally {
|
|
reader.close();
|
|
}
|
|
return builder.toString();
|
|
} catch (IOException ioex) {
|
|
throw new ScriptException(ioex);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Object eval(String script, ScriptContext ctxt) throws ScriptException {
|
|
return eval(createSource(script, ctxt), ctxt);
|
|
}
|
|
|
|
private static Source createSource(String script, ScriptContext ctxt) throws ScriptException {
|
|
log.trace("createSource ctxt {}", ctxt);
|
|
final Object val = ctxt.getAttribute(ScriptEngine.FILENAME);
|
|
if (val == null) {
|
|
return Source.newBuilder(ID, script, "<eval>").buildLiteral();
|
|
} else {
|
|
try {
|
|
return Source.newBuilder(ID, new File(val.toString())).content(script).build();
|
|
} catch (IOException ioex) {
|
|
throw new ScriptException(ioex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void updateDelegatingIOStreams(Context polyglotContext, ScriptContext scriptContext) {
|
|
Value polyglotBindings = polyglotContext.getPolyglotBindings();
|
|
((DelegatingOutputStream) polyglotBindings.getMember(OUT_SYMBOL).asProxyObject()).setWriter(scriptContext.getWriter());
|
|
((DelegatingOutputStream) polyglotBindings.getMember(ERR_SYMBOL).asProxyObject()).setWriter(scriptContext.getErrorWriter());
|
|
((DelegatingInputStream) polyglotBindings.getMember(IN_SYMBOL).asProxyObject()).setReader(scriptContext.getReader());
|
|
}
|
|
|
|
private Object eval(Source source, ScriptContext scriptContext) throws ScriptException {
|
|
log.debug("eval({},{}) called", source, scriptContext);
|
|
GraalJSBindings engineBindings = getOrCreateGraalJSBindings(scriptContext);
|
|
log.trace(" engineBindings {}", engineBindings);
|
|
Context polyglotContext = engineBindings.getContext();
|
|
updateDelegatingIOStreams(polyglotContext, scriptContext);
|
|
try {
|
|
log.trace(" try evalCalled = {}", evalCalled);
|
|
if (!evalCalled) {
|
|
jrunscriptInitWorkaround(source, polyglotContext);
|
|
}
|
|
log.trace(" start engineBindings.importGlobalBindings");
|
|
engineBindings.importGlobalBindings(scriptContext);
|
|
log.trace(" return polyglotContext.eval(..)");
|
|
return polyglotContext.eval(source).as(Object.class);
|
|
} catch (PolyglotException e) {
|
|
log.warn("Exception in eval", e.getCause());
|
|
throw toScriptException(e);
|
|
} finally {
|
|
evalCalled = true;
|
|
}
|
|
}
|
|
|
|
private static ScriptException toScriptException(PolyglotException ex) {
|
|
ScriptException sex;
|
|
if (ex.isHostException()) {
|
|
Throwable hostException = ex.asHostException();
|
|
// ScriptException (unlike almost any other exception) does not
|
|
// accept Throwable cause (requires the cause to be Exception)
|
|
Exception cause;
|
|
if (hostException instanceof Exception) {
|
|
cause = (Exception) hostException;
|
|
} else {
|
|
cause = new Exception(hostException);
|
|
}
|
|
// Make the host exception accessible through the cause chain
|
|
sex = new ScriptException(cause);
|
|
// Re-use the stack-trace of PolyglotException (with guest-language stack-frames)
|
|
sex.setStackTrace(ex.getStackTrace());
|
|
} else {
|
|
sex = new ScriptException(ex);
|
|
}
|
|
return sex;
|
|
}
|
|
|
|
private GraalJSBindings getOrCreateGraalJSBindings(ScriptContext scriptContext) {
|
|
log.debug("getOrCreateGraalJSBindings invoked with {}", scriptContext);
|
|
Bindings engineB = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
|
|
if (engineB instanceof GraalJSBindings) {
|
|
log.trace(" return engineB: {}", engineB);
|
|
return ((GraalJSBindings) engineB);
|
|
} else {
|
|
GraalJSBindings bindings = new GraalJSBindings(createContext(engineB), scriptContext);
|
|
bindings.putAll(engineB);
|
|
log.trace(" recreate bindings: {} engineB {}", bindings, engineB);
|
|
return bindings;
|
|
}
|
|
}
|
|
|
|
private Context createContext(Bindings engineB) {
|
|
log.debug("createContext bindings: {}", engineB);
|
|
Object ctx = engineB.get(POLYGLOT_CONTEXT);
|
|
if (!(ctx instanceof Context)) {
|
|
Context.Builder builder = contextConfig;
|
|
log.trace(" invoking MAGIC_OPTION_SETTERS");
|
|
for (MagicBindingsOptionSetter optionSetter : MAGIC_OPTION_SETTERS) {
|
|
log.trace(" optionSetter: {}", optionSetter);
|
|
Object value = engineB.get(optionSetter.getOptionKey());
|
|
if (value != null) {
|
|
log.trace(" value: {}", value);
|
|
builder = optionSetter.setOption(builder, value);
|
|
engineB.remove(optionSetter.getOptionKey());
|
|
}
|
|
}
|
|
ctx = createDefaultContext(builder);
|
|
log.trace(" createDefaultContext returns {} for {}", ctx, POLYGLOT_CONTEXT);
|
|
engineB.put(POLYGLOT_CONTEXT, ctx);
|
|
}
|
|
return (Context) ctx;
|
|
}
|
|
|
|
@Override
|
|
public GraalJSEngineFactory getFactory() {
|
|
return factory;
|
|
}
|
|
|
|
@Override
|
|
public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {
|
|
log.trace("invokeMethod {} {}", thiz, name);
|
|
if (thiz == null) {
|
|
throw new IllegalArgumentException("thiz is not a valid object.");
|
|
}
|
|
GraalJSBindings engineBindings = getOrCreateGraalJSBindings(context);
|
|
engineBindings.importGlobalBindings(context);
|
|
Value thisValue = engineBindings.getContext().asValue(thiz);
|
|
|
|
if (!thisValue.canInvokeMember(name)) {
|
|
if (!thisValue.hasMember(name)) {
|
|
throw noSuchMethod(name);
|
|
} else {
|
|
throw notCallable(name);
|
|
}
|
|
}
|
|
try {
|
|
return thisValue.invokeMember(name, args).as(Object.class);
|
|
} catch (PolyglotException e) {
|
|
throw toScriptException(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
|
|
log.trace("invokeFunction {}", name);
|
|
GraalJSBindings engineBindings = getOrCreateGraalJSBindings(context);
|
|
engineBindings.importGlobalBindings(context);
|
|
Value function = engineBindings.getContext().getBindings(ID).getMember(name);
|
|
|
|
if (function == null) {
|
|
throw noSuchMethod(name);
|
|
} else if (!function.canExecute()) {
|
|
throw notCallable(name);
|
|
}
|
|
try {
|
|
return function.execute(args).as(Object.class);
|
|
} catch (PolyglotException e) {
|
|
throw toScriptException(e);
|
|
}
|
|
}
|
|
|
|
private static NoSuchMethodException noSuchMethod(String name) throws NoSuchMethodException {
|
|
throw new NoSuchMethodException(name);
|
|
}
|
|
|
|
private static NoSuchMethodException notCallable(String name) throws NoSuchMethodException {
|
|
throw new NoSuchMethodException(name + " is not a function");
|
|
}
|
|
|
|
@Override
|
|
public <T> T getInterface(Class<T> clasz) {
|
|
checkInterface(clasz);
|
|
log.trace("getInterface of {}", clasz);
|
|
var retval = getInterfaceInner(evalInternal(getPolyglotContext(), "locals()"), clasz); // was "this" in Javascript
|
|
log.trace(" returns {}", retval);
|
|
return retval;
|
|
}
|
|
|
|
@Override
|
|
public <T> T getInterface(Object thiz, Class<T> clasz) {
|
|
if (thiz == null) {
|
|
throw new IllegalArgumentException("this cannot be null");
|
|
}
|
|
checkInterface(clasz);
|
|
Value thisValue = getPolyglotContext().asValue(thiz);
|
|
checkThis(thisValue);
|
|
return getInterfaceInner(thisValue, clasz);
|
|
}
|
|
|
|
private static void checkInterface(Class<?> clasz) {
|
|
if (clasz == null || !clasz.isInterface()) {
|
|
throw new IllegalArgumentException("interface Class expected in getInterface");
|
|
}
|
|
}
|
|
|
|
private static void checkThis(Value thiz) {
|
|
if (thiz.isHostObject() || !thiz.hasMembers()) {
|
|
throw new IllegalArgumentException("getInterface cannot be called on non-script object");
|
|
}
|
|
}
|
|
|
|
private static <T> T getInterfaceInner(Value thiz, Class<T> iface) {
|
|
if (!isInterfaceImplemented(iface, thiz)) {
|
|
return null;
|
|
}
|
|
return thiz.as(iface);
|
|
}
|
|
|
|
@Override
|
|
public CompiledScript compile(String script) throws ScriptException {
|
|
Source source = createSource(script, getContext());
|
|
return compile(source);
|
|
}
|
|
|
|
@Override
|
|
public CompiledScript compile(Reader reader) throws ScriptException {
|
|
Source source = createSource(read(reader), getContext());
|
|
return compile(source);
|
|
}
|
|
|
|
private CompiledScript compile(Source source) throws ScriptException {
|
|
checkSyntax(source);
|
|
return new CompiledScript() {
|
|
@Override
|
|
public ScriptEngine getEngine() {
|
|
return GraalJSScriptEngine.this;
|
|
}
|
|
|
|
@Override
|
|
public Object eval(ScriptContext ctx) throws ScriptException {
|
|
return GraalJSScriptEngine.this.eval(source, ctx);
|
|
}
|
|
};
|
|
}
|
|
|
|
private void checkSyntax(Source source) throws ScriptException {
|
|
try {
|
|
getPolyglotContext().parse(source);
|
|
} catch (PolyglotException pex) {
|
|
throw toScriptException(pex);
|
|
}
|
|
}
|
|
|
|
private static class DelegatingInputStream extends InputStream implements Proxy {
|
|
|
|
private Reader reader;
|
|
private CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
|
|
private CharBuffer charBuffer = CharBuffer.allocate(2);
|
|
private ByteBuffer byteBuffer = ByteBuffer.allocate((int) encoder.maxBytesPerChar() * 2);
|
|
|
|
DelegatingInputStream() {
|
|
byteBuffer.flip();
|
|
}
|
|
|
|
@Override
|
|
public int read() throws IOException {
|
|
if (reader != null) {
|
|
while (!byteBuffer.hasRemaining()) {
|
|
int c = reader.read();
|
|
if (c == -1) {
|
|
return -1;
|
|
}
|
|
byteBuffer.clear();
|
|
charBuffer.put((char) c);
|
|
charBuffer.flip();
|
|
encoder.encode(charBuffer, byteBuffer, false);
|
|
charBuffer.compact();
|
|
byteBuffer.flip();
|
|
}
|
|
return byteBuffer.get();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void setReader(Reader reader) {
|
|
this.reader = reader;
|
|
}
|
|
|
|
}
|
|
|
|
private static class DelegatingOutputStream extends OutputStream implements Proxy {
|
|
|
|
private Writer writer;
|
|
private CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
|
|
private ByteBuffer byteBuffer = ByteBuffer.allocate((int) StandardCharsets.UTF_8.newEncoder().maxBytesPerChar() * 2);
|
|
private CharBuffer charBuffer = CharBuffer.allocate(byteBuffer.capacity() * (int) decoder.maxCharsPerByte());
|
|
|
|
@Override
|
|
public void write(int b) throws IOException {
|
|
if (writer != null) {
|
|
byteBuffer.put((byte) b);
|
|
byteBuffer.flip();
|
|
decoder.decode(byteBuffer, charBuffer, false);
|
|
byteBuffer.compact();
|
|
charBuffer.flip();
|
|
while (charBuffer.hasRemaining()) {
|
|
char c = charBuffer.get();
|
|
writer.write(c);
|
|
}
|
|
charBuffer.clear();
|
|
writer.flush(); // was needed to get tests to actually print
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void flush() throws IOException {
|
|
if (writer != null) {
|
|
writer.flush();
|
|
}
|
|
}
|
|
|
|
void setWriter(Writer writer) {
|
|
this.writer = writer;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Creates a new GraalJSScriptEngine with default configuration.
|
|
*
|
|
* @see #create(Engine, Context.Builder) to customize the configuration.
|
|
* @return A new script engine created with null parameters
|
|
*/
|
|
public static GraalJSScriptEngine create() {
|
|
log.debug("create invoked");
|
|
return create(null, null);
|
|
}
|
|
|
|
/**
|
|
* Creates a new GraalJS script engine from a polyglot Engine instance with a base configuration
|
|
* for new polyglot {@link Context} instances. Polyglot context instances can be accessed from
|
|
* {@link ScriptContext} instances using {@link #getPolyglotContext()}. The
|
|
* {@link Builder#out(OutputStream) out},{@link Builder#err(OutputStream) err} and
|
|
* {@link Builder#in(InputStream) in} stream configuration are not inherited from the provided
|
|
* polyglot context config. Instead {@link ScriptContext} output and input streams are used.
|
|
*
|
|
* @param engine the engine to be used for context configurations or <code>null</code> if a
|
|
* default engine should be used.
|
|
* @param newContextConfig a base configuration to create new context instances or
|
|
* <code>null</code> if the default configuration should be used to construct new
|
|
* context instances.
|
|
* @return A new script engine
|
|
*/
|
|
public static GraalJSScriptEngine create(Engine engine, Context.Builder newContextConfig) {
|
|
log.debug("create(stuff) invoked");
|
|
return new GraalJSScriptEngine(null, engine, newContextConfig);
|
|
}
|
|
|
|
private static boolean isInterfaceImplemented(final Class<?> iface, final Value obj) {
|
|
for (final Method method : iface.getMethods()) {
|
|
// ignore methods of java.lang.Object class
|
|
if (method.getDeclaringClass() == Object.class) {
|
|
continue;
|
|
}
|
|
|
|
// skip check for default methods - non-abstract, interface methods
|
|
if (!Modifier.isAbstract(method.getModifiers())) {
|
|
continue;
|
|
}
|
|
|
|
if (!obj.canInvokeMember(method.getName())) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Detects jrunscript "init.js" and installs a JSAdapter polyfill if needed.
|
|
*/
|
|
private static void jrunscriptInitWorkaround(Source source, Context polyglotContext) {
|
|
log.trace("jrunscriptInitWorkaround with {}, {}", source, polyglotContext);
|
|
if (source.getName().equals(JRUNSCRIPT_INIT_NAME)) {
|
|
log.trace(" passed 1st if with match on {}", source.getName());
|
|
String initCode = source.getCharacters().toString();
|
|
log.trace(" creates initCode = {}", initCode);
|
|
if (initCode.contains("jrunscript") && initCode.contains("JSAdapter") && !polyglotContext.getBindings(ID).hasMember("JSAdapter")) {
|
|
log.trace(" polyglotContext.eval {} {}", ID, JSADAPTER_POLYFILL);
|
|
polyglotContext.eval(ID, JSADAPTER_POLYFILL);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final String JRUNSCRIPT_INIT_NAME = "<system-init>";
|
|
private static final String JSADAPTER_POLYFILL = "this.JSAdapter || " +
|
|
"Object.defineProperty(this, \"JSAdapter\", {configurable:true, writable:true, enumerable: false, value: function(t) {\n" +
|
|
" var target = {};\n" +
|
|
" var handler = {\n" +
|
|
" get: function(target, name) {return typeof t.__get__ == 'function' ? t.__get__.call(target, name) : undefined;},\n" +
|
|
" has: function(target, name) {return typeof t.__has__ == 'function' ? t.__has__.call(target, name) : false;},\n" +
|
|
" deleteProperty: function(target, name) {return typeof t.__delete__ == 'function' ? t.__delete__.call(target, name) : true;},\n" +
|
|
" set: function(target, name, value) {return typeof t.__put__ == 'function' ? t.__put__.call(target, name, value) : undefined;},\n" +
|
|
" ownKeys: function(target) {return typeof t.__getIds__ == 'function' ? t.__getIds__.call(target) : [];},\n" +
|
|
" }\n" +
|
|
" return new Proxy(target, handler);\n" +
|
|
"}});\n";
|
|
|
|
private static IllegalArgumentException magicOptionValueErrorBool(String name, Object v) {
|
|
return new IllegalArgumentException(String.format("failed to set graal-js option \"%s\": expected a boolean value, got \"%s\"", name, v));
|
|
}
|
|
|
|
private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GraalJSScriptEngine.class);
|
|
}
|