/* * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.nashorn.api.scripting; import static jdk.nashorn.internal.runtime.ECMAErrors.referenceError; import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.nio.charset.Charset; import java.security.AccessControlContext; import java.security.AccessController; import java.security.Permissions; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.ProtectionDomain; import java.text.MessageFormat; import java.util.Locale; import java.util.ResourceBundle; 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 javax.script.ScriptException; import javax.script.SimpleBindings; import jdk.nashorn.internal.objects.Global; import jdk.nashorn.internal.runtime.Context; import jdk.nashorn.internal.runtime.ErrorManager; import jdk.nashorn.internal.runtime.Property; import jdk.nashorn.internal.runtime.ScriptFunction; import jdk.nashorn.internal.runtime.ScriptObject; import jdk.nashorn.internal.runtime.ScriptRuntime; import jdk.nashorn.internal.runtime.Source; import jdk.nashorn.internal.runtime.linker.JavaAdapterFactory; import jdk.nashorn.internal.runtime.options.Options; /** * JSR-223 compliant script engine for Nashorn. Instances are not created directly, but rather returned through * {@link NashornScriptEngineFactory#getScriptEngine()}. Note that this engine implements the {@link Compilable} and * {@link Invocable} interfaces, allowing for efficient precompilation and repeated execution of scripts. * @see NashornScriptEngineFactory */ public final class NashornScriptEngine extends AbstractScriptEngine implements Compilable, Invocable { /** * Key used to associate Nashorn global object mirror with arbitrary Bindings instance. */ public static final String NASHORN_GLOBAL = "nashorn.global"; // commonly used access control context objects private static AccessControlContext createPermAccCtxt(final String permName) { final Permissions perms = new Permissions(); perms.add(new RuntimePermission(permName)); return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) }); } private static final AccessControlContext CREATE_CONTEXT_ACC_CTXT = createPermAccCtxt(Context.NASHORN_CREATE_CONTEXT); private static final AccessControlContext CREATE_GLOBAL_ACC_CTXT = createPermAccCtxt(Context.NASHORN_CREATE_GLOBAL); // the factory that created this engine private final ScriptEngineFactory factory; // underlying nashorn Context - 1:1 with engine instance private final Context nashornContext; // do we want to share single Nashorn global instance across ENGINE_SCOPEs? private final boolean _global_per_engine; // This is the initial default Nashorn global object. // This is used as "shared" global if above option is true. private final Global global; // initialized bit late to be made 'final'. // Property object for "context" property of global object. private volatile Property contextProperty; // default options passed to Nashorn Options object private static final String[] DEFAULT_OPTIONS = new String[] { "-doe" }; // Nashorn script engine error message management private static final String MESSAGES_RESOURCE = "jdk.nashorn.api.scripting.resources.Messages"; private static final ResourceBundle MESSAGES_BUNDLE; static { MESSAGES_BUNDLE = ResourceBundle.getBundle(MESSAGES_RESOURCE, Locale.getDefault()); } // helper to get Nashorn script engine error message private static String getMessage(final String msgId, final String... args) { try { return new MessageFormat(MESSAGES_BUNDLE.getString(msgId)).format(args); } catch (final java.util.MissingResourceException e) { throw new RuntimeException("no message resource found for message id: "+ msgId); } } // load engine.js and return content as a char[] @SuppressWarnings("resource") private static char[] loadEngineJSSource() { final String script = "resources/engine.js"; try { final InputStream is = AccessController.doPrivileged( new PrivilegedExceptionAction() { @Override public InputStream run() throws Exception { final URL url = NashornScriptEngine.class.getResource(script); return url.openStream(); } }); return Source.readFully(new InputStreamReader(is)); } catch (final PrivilegedActionException | IOException e) { if (Context.DEBUG) { e.printStackTrace(); } throw new RuntimeException(e); } } // Source object for engine.js private static final Source ENGINE_SCRIPT_SRC = new Source(NashornException.ENGINE_SCRIPT_SOURCE_NAME, loadEngineJSSource()); NashornScriptEngine(final NashornScriptEngineFactory factory, final ClassLoader appLoader) { this(factory, DEFAULT_OPTIONS, appLoader); } NashornScriptEngine(final NashornScriptEngineFactory factory, final String[] args, final ClassLoader appLoader) { this.factory = factory; final Options options = new Options("nashorn"); options.process(args); // throw ParseException on first error from script final ErrorManager errMgr = new Context.ThrowErrorManager(); // create new Nashorn Context this.nashornContext = AccessController.doPrivileged(new PrivilegedAction() { @Override public Context run() { try { return new Context(options, errMgr, appLoader); } catch (final RuntimeException e) { if (Context.DEBUG) { e.printStackTrace(); } throw e; } } }, CREATE_CONTEXT_ACC_CTXT); // cache this option that is used often this._global_per_engine = nashornContext.getEnv()._global_per_engine; // create new global object this.global = createNashornGlobal(context); // set the default ENGINE_SCOPE object for the default context context.setBindings(new ScriptObjectMirror(global, global), ScriptContext.ENGINE_SCOPE); } @Override public Object eval(final Reader reader, final ScriptContext ctxt) throws ScriptException { return evalImpl(makeSource(reader, ctxt), ctxt); } @Override public Object eval(final String script, final ScriptContext ctxt) throws ScriptException { return evalImpl(makeSource(script, ctxt), ctxt); } @Override public ScriptEngineFactory getFactory() { return factory; } @Override public Bindings createBindings() { if (_global_per_engine) { // just create normal SimpleBindings. // We use same 'global' for all Bindings. return new SimpleBindings(); } return createGlobalMirror(null); } // Compilable methods @Override public CompiledScript compile(final Reader reader) throws ScriptException { return asCompiledScript(makeSource(reader, context)); } @Override public CompiledScript compile(final String str) throws ScriptException { return asCompiledScript(makeSource(str, context)); } // Invocable methods @Override public Object invokeFunction(final String name, final Object... args) throws ScriptException, NoSuchMethodException { return invokeImpl(null, name, args); } @Override public Object invokeMethod(final Object thiz, final String name, final Object... args) throws ScriptException, NoSuchMethodException { if (thiz == null) { throw new IllegalArgumentException(getMessage("thiz.cannot.be.null")); } return invokeImpl(thiz, name, args); } @Override public T getInterface(final Class clazz) { return getInterfaceInner(null, clazz); } @Override public T getInterface(final Object thiz, final Class clazz) { if (thiz == null) { throw new IllegalArgumentException(getMessage("thiz.cannot.be.null")); } return getInterfaceInner(thiz, clazz); } // These are called from the "engine.js" script /** * This hook is used to search js global variables exposed from Java code. * * @param self 'this' passed from the script * @param ctxt current ScriptContext in which name is searched * @param name name of the variable searched * @return the value of the named variable */ public Object __noSuchProperty__(final Object self, final ScriptContext ctxt, final String name) { if (ctxt != null) { final int scope = ctxt.getAttributesScope(name); final Global ctxtGlobal = getNashornGlobalFrom(ctxt); if (scope != -1) { return ScriptObjectMirror.unwrap(ctxt.getAttribute(name, scope), ctxtGlobal); } if (self == UNDEFINED) { // scope access and so throw ReferenceError throw referenceError(ctxtGlobal, "not.defined", name); } } return UNDEFINED; } // Implementation only below this point private static Source makeSource(final Reader reader, final ScriptContext ctxt) throws ScriptException { try { if (reader instanceof URLReader) { final URL url = ((URLReader)reader).getURL(); final Charset cs = ((URLReader)reader).getCharset(); return new Source(url.toString(), url, cs); } return new Source(getScriptName(ctxt), Source.readFully(reader)); } catch (final IOException e) { throw new ScriptException(e); } } private static Source makeSource(final String src, final ScriptContext ctxt) { return new Source(getScriptName(ctxt), src); } private static String getScriptName(final ScriptContext ctxt) { final Object val = ctxt.getAttribute(ScriptEngine.FILENAME); return (val != null) ? val.toString() : ""; } private T getInterfaceInner(final Object thiz, final Class clazz) { if (clazz == null || !clazz.isInterface()) { throw new IllegalArgumentException(getMessage("interface.class.expected")); } // perform security access check as early as possible final SecurityManager sm = System.getSecurityManager(); if (sm != null) { if (! Modifier.isPublic(clazz.getModifiers())) { throw new SecurityException(getMessage("implementing.non.public.interface", clazz.getName())); } Context.checkPackageAccess(clazz); } ScriptObject realSelf = null; Global realGlobal = null; if(thiz == null) { // making interface out of global functions realSelf = realGlobal = getNashornGlobalFrom(context); } else if (thiz instanceof ScriptObjectMirror) { final ScriptObjectMirror mirror = (ScriptObjectMirror)thiz; realSelf = mirror.getScriptObject(); realGlobal = mirror.getHomeGlobal(); if (! isOfContext(realGlobal, nashornContext)) { throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); } } else if (thiz instanceof ScriptObject) { // called from script code. realSelf = (ScriptObject)thiz; realGlobal = Context.getGlobal(); if (realGlobal == null) { throw new IllegalArgumentException(getMessage("no.current.nashorn.global")); } if (! isOfContext(realGlobal, nashornContext)) { throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); } } if (realSelf == null) { throw new IllegalArgumentException(getMessage("interface.on.non.script.object")); } try { final Global oldGlobal = Context.getGlobal(); final boolean globalChanged = (oldGlobal != realGlobal); try { if (globalChanged) { Context.setGlobal(realGlobal); } if (! isInterfaceImplemented(clazz, realSelf)) { return null; } return clazz.cast(JavaAdapterFactory.getConstructor(realSelf.getClass(), clazz, MethodHandles.publicLookup()).invoke(realSelf)); } finally { if (globalChanged) { Context.setGlobal(oldGlobal); } } } catch(final RuntimeException|Error e) { throw e; } catch(final Throwable t) { throw new RuntimeException(t); } } // Retrieve nashorn Global object for a given ScriptContext object private Global getNashornGlobalFrom(final ScriptContext ctxt) { if (_global_per_engine) { // shared single global object for all ENGINE_SCOPE Bindings return global; } final Bindings bindings = ctxt.getBindings(ScriptContext.ENGINE_SCOPE); // is this Nashorn's own Bindings implementation? if (bindings instanceof ScriptObjectMirror) { final Global glob = globalFromMirror((ScriptObjectMirror)bindings); if (glob != null) { return glob; } } // Arbitrary user Bindings implementation. Look for NASHORN_GLOBAL in it! Object scope = bindings.get(NASHORN_GLOBAL); if (scope instanceof ScriptObjectMirror) { final Global glob = globalFromMirror((ScriptObjectMirror)scope); if (glob != null) { return glob; } } // We didn't find associated nashorn global mirror in the Bindings given! // Create new global instance mirror and associate with the Bindings. final ScriptObjectMirror mirror = createGlobalMirror(ctxt); bindings.put(NASHORN_GLOBAL, mirror); return mirror.getHomeGlobal(); } // Retrieve nashorn Global object from a given ScriptObjectMirror private Global globalFromMirror(final ScriptObjectMirror mirror) { ScriptObject sobj = mirror.getScriptObject(); if (sobj instanceof Global && isOfContext((Global)sobj, nashornContext)) { return (Global)sobj; } return null; } // Create a new ScriptObjectMirror wrapping a newly created Nashorn Global object private ScriptObjectMirror createGlobalMirror(final ScriptContext ctxt) { final Global newGlobal = createNashornGlobal(ctxt); return new ScriptObjectMirror(newGlobal, newGlobal); } // Create a new Nashorn Global object private Global createNashornGlobal(final ScriptContext ctxt) { final Global newGlobal = AccessController.doPrivileged(new PrivilegedAction() { @Override public Global run() { try { return nashornContext.newGlobal(); } catch (final RuntimeException e) { if (Context.DEBUG) { e.printStackTrace(); } throw e; } } }, CREATE_GLOBAL_ACC_CTXT); nashornContext.initGlobal(newGlobal); final int NON_ENUMERABLE_CONSTANT = Property.NOT_ENUMERABLE | Property.NOT_CONFIGURABLE | Property.NOT_WRITABLE; // current ScriptContext exposed as "context" // "context" is non-writable from script - but script engine still // needs to set it and so save the context Property object contextProperty = newGlobal.addOwnProperty("context", NON_ENUMERABLE_CONSTANT, ctxt); // current ScriptEngine instance exposed as "engine". We added @SuppressWarnings("LeakingThisInConstructor") as // NetBeans identifies this assignment as such a leak - this is a false positive as we're setting this property // in the Global of a Context we just created - both the Context and the Global were just created and can not be // seen from another thread outside of this constructor. newGlobal.addOwnProperty("engine", NON_ENUMERABLE_CONSTANT, this); // global script arguments with undefined value newGlobal.addOwnProperty("arguments", Property.NOT_ENUMERABLE, UNDEFINED); // file name default is null newGlobal.addOwnProperty(ScriptEngine.FILENAME, Property.NOT_ENUMERABLE, null); // evaluate engine.js initialization script this new global object try { evalImpl(compileImpl(ENGINE_SCRIPT_SRC, newGlobal), ctxt, newGlobal); } catch (final ScriptException exp) { throw new RuntimeException(exp); } return newGlobal; } // scripts should see "context" and "engine" as variables in the given global object private void setContextVariables(final Global ctxtGlobal, final ScriptContext ctxt) { // set "context" global variable via contextProperty - because this // property is non-writable contextProperty.setObjectValue(ctxtGlobal, ctxtGlobal, ctxt, false); Object args = ScriptObjectMirror.unwrap(ctxt.getAttribute("arguments"), ctxtGlobal); if (args == null || args == UNDEFINED) { args = ScriptRuntime.EMPTY_ARRAY; } // if no arguments passed, expose it if (! (args instanceof ScriptObject)) { args = ctxtGlobal.wrapAsObject(args); ctxtGlobal.set("arguments", args, false); } } private Object invokeImpl(final Object selfObject, final String name, final Object... args) throws ScriptException, NoSuchMethodException { name.getClass(); // null check Global invokeGlobal = null; ScriptObjectMirror selfMirror = null; if (selfObject instanceof ScriptObjectMirror) { selfMirror = (ScriptObjectMirror)selfObject; if (! isOfContext(selfMirror.getHomeGlobal(), nashornContext)) { throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); } invokeGlobal = selfMirror.getHomeGlobal(); } else if (selfObject instanceof ScriptObject) { // invokeMethod called from script code - in which case we may get 'naked' ScriptObject // Wrap it with oldGlobal to make a ScriptObjectMirror for the same. final Global oldGlobal = Context.getGlobal(); invokeGlobal = oldGlobal; if (oldGlobal == null) { throw new IllegalArgumentException(getMessage("no.current.nashorn.global")); } if (! isOfContext(oldGlobal, nashornContext)) { throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); } selfMirror = (ScriptObjectMirror)ScriptObjectMirror.wrap(selfObject, oldGlobal); } else if (selfObject == null) { // selfObject is null => global function call final Global ctxtGlobal = getNashornGlobalFrom(context); invokeGlobal = ctxtGlobal; selfMirror = (ScriptObjectMirror)ScriptObjectMirror.wrap(ctxtGlobal, ctxtGlobal); } if (selfMirror != null) { try { return ScriptObjectMirror.translateUndefined(selfMirror.callMember(name, args)); } catch (final Exception e) { final Throwable cause = e.getCause(); if (cause instanceof NoSuchMethodException) { throw (NoSuchMethodException)cause; } throwAsScriptException(e, invokeGlobal); throw new AssertionError("should not reach here"); } } // Non-script object passed as selfObject throw new IllegalArgumentException(getMessage("interface.on.non.script.object")); } private Object evalImpl(final Source src, final ScriptContext ctxt) throws ScriptException { return evalImpl(compileImpl(src, ctxt), ctxt); } private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt) throws ScriptException { return evalImpl(script, ctxt, getNashornGlobalFrom(ctxt)); } private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt, final Global ctxtGlobal) throws ScriptException { if (script == null) { return null; } final Global oldGlobal = Context.getGlobal(); final boolean globalChanged = (oldGlobal != ctxtGlobal); try { if (globalChanged) { Context.setGlobal(ctxtGlobal); } // set ScriptContext variables if ctxt is non-null if (ctxt != null) { setContextVariables(ctxtGlobal, ctxt); } return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(ScriptRuntime.apply(script, ctxtGlobal), ctxtGlobal)); } catch (final Exception e) { throwAsScriptException(e, ctxtGlobal); throw new AssertionError("should not reach here"); } finally { if (globalChanged) { Context.setGlobal(oldGlobal); } } } private static void throwAsScriptException(final Exception e, final Global global) throws ScriptException { if (e instanceof ScriptException) { throw (ScriptException)e; } else if (e instanceof NashornException) { final NashornException ne = (NashornException)e; final ScriptException se = new ScriptException( ne.getMessage(), ne.getFileName(), ne.getLineNumber(), ne.getColumnNumber()); ne.initEcmaError(global); se.initCause(e); throw se; } else if (e instanceof RuntimeException) { throw (RuntimeException)e; } else { // wrap any other exception as ScriptException throw new ScriptException(e); } } private CompiledScript asCompiledScript(final Source source) throws ScriptException { final ScriptFunction func = compileImpl(source, context); return new CompiledScript() { @Override public Object eval(final ScriptContext ctxt) throws ScriptException { final Global globalObject = getNashornGlobalFrom(ctxt); // Are we running the script in the correct global? if (func.getScope() == globalObject) { return evalImpl(func, ctxt, globalObject); } // ScriptContext with a different global. Compile again! // Note that we may still hit per-global compilation cache. return evalImpl(compileImpl(source, ctxt), ctxt, globalObject); } @Override public ScriptEngine getEngine() { return NashornScriptEngine.this; } }; } private ScriptFunction compileImpl(final Source source, final ScriptContext ctxt) throws ScriptException { return compileImpl(source, getNashornGlobalFrom(ctxt)); } private ScriptFunction compileImpl(final Source source, final Global newGlobal) throws ScriptException { final Global oldGlobal = Context.getGlobal(); final boolean globalChanged = (oldGlobal != newGlobal); try { if (globalChanged) { Context.setGlobal(newGlobal); } return nashornContext.compileScript(source, newGlobal); } catch (final Exception e) { throwAsScriptException(e, newGlobal); throw new AssertionError("should not reach here"); } finally { if (globalChanged) { Context.setGlobal(oldGlobal); } } } private static boolean isInterfaceImplemented(final Class iface, final ScriptObject sobj) { 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; } Object obj = sobj.get(method.getName()); if (! (obj instanceof ScriptFunction)) { return false; } } return true; } private static boolean isOfContext(final Global global, final Context context) { return global.isOfContext(context); } }