nashorn/src/jdk/nashorn/api/scripting/NashornScriptEngine.java
author sundar
Mon, 18 Feb 2013 14:41:58 +0530
changeset 16243 f5fdf89e1583
parent 16232 efd57dd90de6
child 16256 f2d9a0c49914
permissions -rw-r--r--
8008305: ScriptEngine.eval should offer the ability to provide a codebase Reviewed-by: lagergren, hannesw, attila

/*
 * 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.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
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 jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.ErrorManager;
import jdk.nashorn.internal.runtime.GlobalObject;
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 {

    private final ScriptEngineFactory factory;
    private final Context             nashornContext;
    private final ScriptObject        global;

    // default options passed to Nashorn Options object
    private static final String[] DEFAULT_OPTIONS = new String[] { "-scripting", "-af", "-doe" };

    NashornScriptEngine(final NashornScriptEngineFactory factory, final ClassLoader appLoader) {
        this(factory, DEFAULT_OPTIONS, appLoader);
    }

    @SuppressWarnings("LeakingThisInConstructor")
    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<Context>() {
            @Override
            public Context run() {
                try {
                    return new Context(options, errMgr, appLoader);
                } catch (final RuntimeException e) {
                    if (Context.DEBUG) {
                        e.printStackTrace();
                    }
                    throw e;
                }
            }
        });

        // create new global object
        this.global =  createNashornGlobal();
        // set the default engine scope for the default context
        context.setBindings(new ScriptObjectMirror(global, global), ScriptContext.ENGINE_SCOPE);

        // evaluate engine initial script
        try {
            evalEngineScript();
        } catch (final ScriptException e) {
            if (Context.DEBUG) {
                e.printStackTrace();
            }
            throw new RuntimeException(e);
        }
    }

    @Override
    public Object eval(final Reader reader, final ScriptContext ctxt) throws ScriptException {
        try {
            if (reader instanceof URLReader) {
                final URL url = ((URLReader)reader).getURL();
                return evalImpl(compileImpl(new Source(url.toString(), url), ctxt), ctxt);
            } else {
                return evalImpl(Source.readFully(reader), ctxt);
            }
        } catch (final IOException e) {
            throw new ScriptException(e);
        }
    }

    @Override
    public Object eval(final String script, final ScriptContext ctxt) throws ScriptException {
        return evalImpl(script.toCharArray(), ctxt);
    }

    @Override
    public ScriptEngineFactory getFactory() {
        return factory;
    }

    @Override
    public Bindings createBindings() {
        final ScriptObject newGlobal = createNashornGlobal();
        return new ScriptObjectMirror(newGlobal, newGlobal);
    }

    // Compilable methods

    @Override
    public CompiledScript compile(final Reader reader) throws ScriptException {
        try {
            return asCompiledScript(compileImpl(Source.readFully(reader), context));
        } catch (final IOException e) {
            throw new ScriptException(e);
        }
    }

    @Override
    public CompiledScript compile(final String str) throws ScriptException {
        return asCompiledScript(compileImpl(str.toCharArray(), 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 self, final String name, final Object... args)
            throws ScriptException, NoSuchMethodException {
        if (self == null) {
            throw new IllegalArgumentException("script object can not be null");
        }
        return invokeImpl(self, name, args);
    }

    private <T> T getInterfaceInner(final Object self, final Class<T> clazz) {
        final Object realSelf;
        final ScriptObject ctxtGlobal = getNashornGlobalFrom(context);
        if(self == null) {
            realSelf = ctxtGlobal;
        } else if (!(self instanceof ScriptObject)) {
            realSelf = ScriptObjectMirror.unwrap(self, ctxtGlobal);
        } else {
            realSelf = self;
        }
        try {
            final ScriptObject oldGlobal = getNashornGlobal();
            try {
                if(oldGlobal != ctxtGlobal) {
                    setNashornGlobal(ctxtGlobal);
                }
                return clazz.cast(JavaAdapterFactory.getConstructor(realSelf.getClass(), clazz).invoke(realSelf));
            } finally {
                if(oldGlobal != ctxtGlobal) {
                    setNashornGlobal(oldGlobal);
                }
            }
        } catch(final RuntimeException|Error e) {
            throw e;
        } catch(final Throwable t) {
            throw new RuntimeException(t);
        }
    }

    @Override
    public <T> T getInterface(final Class<T> clazz) {
        return getInterfaceInner(null, clazz);
    }

    @Override
    public <T> T getInterface(final Object self, final Class<T> clazz) {
        if (self == null) {
            throw new IllegalArgumentException("script object can not be null");
        }
        return getInterfaceInner(self, 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) {
        final int scope = ctxt.getAttributesScope(name);
        final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
        if (scope != -1) {
            return ScriptObjectMirror.unwrap(ctxt.getAttribute(name, scope), ctxtGlobal);
        }

        if (self == UNDEFINED) {
            // scope access and so throw ReferenceError
            referenceError(ctxtGlobal, "not.defined", name);
        }

        return UNDEFINED;
    }

    private ScriptObject getNashornGlobalFrom(final ScriptContext ctxt) {
        final Bindings bindings = ctxt.getBindings(ScriptContext.ENGINE_SCOPE);
        if (bindings instanceof ScriptObjectMirror) {
             ScriptObject sobj = ((ScriptObjectMirror)bindings).getScriptObject();
             if (sobj instanceof GlobalObject) {
                 return sobj;
             }
        }

        // didn't find global object from context given - return the engine-wide global
        return global;
    }

    private ScriptObject createNashornGlobal() {
        final ScriptObject newGlobal = AccessController.doPrivileged(new PrivilegedAction<ScriptObject>() {
            @Override
            public ScriptObject run() {
                try {
                    return nashornContext.newGlobal();
                } catch (final RuntimeException e) {
                    if (Context.DEBUG) {
                        e.printStackTrace();
                    }
                    throw e;
                }
            }
        });

        nashornContext.initGlobal(newGlobal);

        // current ScriptContext exposed as "context"
        newGlobal.addOwnProperty("context", Property.NOT_ENUMERABLE, UNDEFINED);
        // 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", Property.NOT_ENUMERABLE, 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);
        return newGlobal;
    }

    private void evalEngineScript() throws ScriptException {
        evalSupportScript("resources/engine.js", NashornException.ENGINE_SCRIPT_SOURCE_NAME);
    }

    private void evalSupportScript(final String script, final String name) throws ScriptException {
        try {
            final InputStream is = AccessController.doPrivileged(
                    new PrivilegedExceptionAction<InputStream>() {
                        @Override
                        public InputStream run() throws Exception {
                            final URL url = NashornScriptEngine.class.getResource(script);
                            return url.openStream();
                        }
                    });
            put(ScriptEngine.FILENAME, name);
            try (final InputStreamReader isr = new InputStreamReader(is)) {
                eval(isr);
            }
        } catch (final PrivilegedActionException | IOException e) {
            throw new ScriptException(e);
        } finally {
            put(ScriptEngine.FILENAME, null);
        }
    }

    // scripts should see "context" and "engine" as variables
    private void setContextVariables(final ScriptContext ctxt) {
        ctxt.setAttribute("context", ctxt, ScriptContext.ENGINE_SCOPE);
        final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
        ctxtGlobal.set("context", 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
        args = ((GlobalObject)ctxtGlobal).wrapAsObject(args);
        ctxtGlobal.set("arguments", args, false);
    }

    private Object invokeImpl(final Object selfObject, final String name, final Object... args) throws ScriptException, NoSuchMethodException {
        final ScriptObject oldGlobal     = getNashornGlobal();
        final ScriptObject ctxtGlobal    = getNashornGlobalFrom(context);
        final boolean globalChanged = (oldGlobal != ctxtGlobal);

        Object self = globalChanged? ScriptObjectMirror.wrap(selfObject, oldGlobal) : selfObject;

        try {
            if (globalChanged) {
                setNashornGlobal(ctxtGlobal);
            }

            ScriptObject sobj;
            Object       value = null;

            self = ScriptObjectMirror.unwrap(self, ctxtGlobal);

            // FIXME: should convert when self is not ScriptObject
            if (self instanceof ScriptObject) {
                sobj = (ScriptObject)self;
                value = sobj.get(name);
            } else if (self == null) {
                self  = ctxtGlobal;
                sobj  = ctxtGlobal;
                value = sobj.get(name);
            }

            if (value instanceof ScriptFunction) {
                final Object res;
                try {
                    final Object[] modArgs = globalChanged? ScriptObjectMirror.wrapArray(args, oldGlobal) : args;
                    res = ScriptRuntime.checkAndApply((ScriptFunction)value, self, ScriptObjectMirror.unwrapArray(modArgs, ctxtGlobal));
                } catch (final Exception e) {
                    throwAsScriptException(e);
                    throw new AssertionError("should not reach here");
                }
                return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(res, ctxtGlobal));
            }

            throw new NoSuchMethodException(name);
        } finally {
            if (globalChanged) {
                setNashornGlobal(oldGlobal);
            }
        }
    }

    private Object evalImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException {
        return evalImpl(compileImpl(buf, ctxt), ctxt);
    }

    private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt) throws ScriptException {
        if (script == null) {
            return null;
        }
        final ScriptObject oldGlobal = getNashornGlobal();
        final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
        final boolean globalChanged = (oldGlobal != ctxtGlobal);
        try {
            if (globalChanged) {
                setNashornGlobal(ctxtGlobal);
            }

            setContextVariables(ctxt);
            final Object val = ctxt.getAttribute(ScriptEngine.FILENAME);
            final String fileName = (val != null) ? val.toString() : "<eval>";

            // NOTE: FIXME: If this is jrunscript's init.js, we want to run the replacement.
            // This should go away once we fix jrunscript's copy of init.js.
            if ("<system-init>".equals(fileName)) {
                evalSupportScript("resources/init.js", "nashorn:engine/resources/init.js");
                return null;
            }

            Object res = ScriptRuntime.apply(script, ctxtGlobal);
            return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(res, ctxtGlobal));
        } catch (final Exception e) {
            throwAsScriptException(e);
            throw new AssertionError("should not reach here");
        } finally {
            if (globalChanged) {
                setNashornGlobal(oldGlobal);
            }
        }
    }

    private static void throwAsScriptException(final Exception e) 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());
            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 ScriptFunction script) {
        return new CompiledScript() {
            @Override
            public Object eval(final ScriptContext ctxt) throws ScriptException {
                return evalImpl(script, ctxt);
            }
            @Override
            public ScriptEngine getEngine() {
                return NashornScriptEngine.this;
            }
        };
    }

    private ScriptFunction compileImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException {
        final Object val = ctxt.getAttribute(ScriptEngine.FILENAME);
        final String fileName = (val != null) ? val.toString() : "<eval>";
        return compileImpl(new Source(fileName, buf), ctxt);
    }

    private ScriptFunction compileImpl(final Source source, final ScriptContext ctxt) throws ScriptException {
        final ScriptObject oldGlobal = getNashornGlobal();
        final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
        final boolean globalChanged = (oldGlobal != ctxtGlobal);
        try {
            if (globalChanged) {
                setNashornGlobal(ctxtGlobal);
            }

            return nashornContext.compileScript(source, ctxtGlobal);
        } catch (final Exception e) {
            throwAsScriptException(e);
            throw new AssertionError("should not reach here");
        } finally {
            if (globalChanged) {
                setNashornGlobal(oldGlobal);
            }
        }
    }

    // don't make this public!!
    static ScriptObject getNashornGlobal() {
        return Context.getGlobal();
    }

    static void setNashornGlobal(final ScriptObject newGlobal) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
            public Void run() {
               Context.setGlobal(newGlobal);
               return null;
            }
        });
    }
}