       1 /*
       2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
       3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       4  *
       5  * This code is free software; you can redistribute it and/or modify it
       6  * under the terms of the GNU General Public License version 2 only, as
       7  * published by the Free Software Foundation.  Oracle designates this
       8  * particular file as subject to the "Classpath" exception as provided
       9  * by Oracle in the LICENSE file that accompanied this code.
      10  *
      11  * This code is distributed in the hope that it will be useful, but WITHOUT
      12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
      13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
      14  * version 2 for more details (a copy is included in the LICENSE file that
      15  * accompanied this code).
      16  *
      17  * You should have received a copy of the GNU General Public License version
      18  * 2 along with this work; if not, write to the Free Software Foundation,
      19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
      20  *
      21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
      22  * or visit www.oracle.com if you need additional information or have any
      23  * questions.
      24  */
      25
      26 package jdk.nashorn.api.scripting;
      27
      28 import static jdk.nashorn.internal.runtime.Source.sourceFor;
      29
      30 import java.io.IOException;
      31 import java.io.Reader;
      32 import java.lang.invoke.MethodHandles;
      33 import java.lang.reflect.Method;
      34 import java.lang.reflect.Modifier;
      35 import java.security.AccessControlContext;
      36 import java.security.AccessController;
      37 import java.security.Permissions;
      38 import java.security.PrivilegedAction;
      39 import java.security.ProtectionDomain;
      40 import java.text.MessageFormat;
      41 import java.util.Locale;
      42 import java.util.Objects;
      43 import java.util.ResourceBundle;
      44 import javax.script.AbstractScriptEngine;
      45 import javax.script.Bindings;
      46 import javax.script.Compilable;
      47 import javax.script.CompiledScript;
      48 import javax.script.Invocable;
      49 import javax.script.ScriptContext;
      50 import javax.script.ScriptEngine;
      51 import javax.script.ScriptEngineFactory;
      52 import javax.script.ScriptException;
      53 import javax.script.SimpleBindings;
      54 import jdk.nashorn.internal.objects.Global;
      55 import jdk.nashorn.internal.runtime.Context;
      56 import jdk.nashorn.internal.runtime.ErrorManager;
      57 import jdk.nashorn.internal.runtime.ScriptFunction;
      58 import jdk.nashorn.internal.runtime.ScriptObject;
      59 import jdk.nashorn.internal.runtime.ScriptRuntime;
      60 import jdk.nashorn.internal.runtime.Source;
      61 import jdk.nashorn.internal.runtime.linker.JavaAdapterFactory;
      62 import jdk.nashorn.internal.runtime.options.Options;
      63
      64 /**
      65  * JSR-223 compliant script engine for Nashorn. Instances are not created directly, but rather returned through
      66  * {@link NashornScriptEngineFactory#getScriptEngine()}. Note that this engine implements the {@link Compilable} and
      67  * {@link Invocable} interfaces, allowing for efficient precompilation and repeated execution of scripts.
      68  * @see NashornScriptEngineFactory
      69  *
      70  * @since 1.8u40
      71  */
      72 public final class NashornScriptEngine extends AbstractScriptEngine implements Compilable, Invocable {
      73     /**
      74      * Key used to associate Nashorn global object mirror with arbitrary Bindings instance.
      75      */
      76     public static final String NASHORN_GLOBAL = "nashorn.global";
      77
      78     // commonly used access control context objects
      79     private static AccessControlContext createPermAccCtxt(final String permName) {
      80         final Permissions perms = new Permissions();
      81         perms.add(new RuntimePermission(permName));
      82         return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) });
      83     }
      84
      85     private static final AccessControlContext CREATE_CONTEXT_ACC_CTXT = createPermAccCtxt(Context.NASHORN_CREATE_CONTEXT);
      86     private static final AccessControlContext CREATE_GLOBAL_ACC_CTXT  = createPermAccCtxt(Context.NASHORN_CREATE_GLOBAL);
      87
      88     // the factory that created this engine
      89     private final ScriptEngineFactory factory;
      90     // underlying nashorn Context - 1:1 with engine instance
      91     private final Context             nashornContext;
      92     // do we want to share single Nashorn global instance across ENGINE_SCOPEs?
      93     private final boolean             _global_per_engine;
      94     // This is the initial default Nashorn global object.
      95     // This is used as "shared" global if above option is true.
      96     private final Global              global;
      97
      98     // Nashorn script engine error message management
      99     private static final String MESSAGES_RESOURCE = "jdk.nashorn.api.scripting.resources.Messages";
     100
     101     private static final ResourceBundle MESSAGES_BUNDLE;
     102     static {
     103         MESSAGES_BUNDLE = ResourceBundle.getBundle(MESSAGES_RESOURCE, Locale.getDefault());
     104     }
     105
     106     // helper to get Nashorn script engine error message
     107     private static String getMessage(final String msgId, final String... args) {
     108         try {
     109             return new MessageFormat(MESSAGES_BUNDLE.getString(msgId)).format(args);
     110         } catch (final java.util.MissingResourceException e) {
     111             throw new RuntimeException("no message resource found for message id: "+ msgId);
     112         }
     113     }
     114
     115     NashornScriptEngine(final NashornScriptEngineFactory factory, final String[] args, final ClassLoader appLoader, final ClassFilter classFilter) {
     116         assert args != null : "null argument array";
     117         this.factory = factory;
     118         final Options options = new Options("nashorn");
     119         options.process(args);
     120
     121         // throw ParseException on first error from script
     122         final ErrorManager errMgr = new Context.ThrowErrorManager();
     123         // create new Nashorn Context
     124         this.nashornContext = AccessController.doPrivileged(new PrivilegedAction<Context>() {
     125             @Override
     126             public Context run() {
     127                 try {
     128                     return new Context(options, errMgr, appLoader, classFilter);
     129                 } catch (final RuntimeException e) {
     130                     if (Context.DEBUG) {
     131                         e.printStackTrace();
     132                     }
     133                     throw e;
     134                 }
     135             }
     136         }, CREATE_CONTEXT_ACC_CTXT);
     137
     138         // cache this option that is used often
     139         this._global_per_engine = nashornContext.getEnv()._global_per_engine;
     140
     141         // create new global object
     142         this.global = createNashornGlobal();
     143         // set the default ENGINE_SCOPE object for the default context
     144         context.setBindings(new ScriptObjectMirror(global, global), ScriptContext.ENGINE_SCOPE);
     145     }
     146
     147     @Override
     148     public Object eval(final Reader reader, final ScriptContext ctxt) throws ScriptException {
     149         return evalImpl(makeSource(reader, ctxt), ctxt);
     150     }
     151
     152     @Override
     153     public Object eval(final String script, final ScriptContext ctxt) throws ScriptException {
     154         return evalImpl(makeSource(script, ctxt), ctxt);
     155     }
     156
     157     @Override
     158     public ScriptEngineFactory getFactory() {
     159         return factory;
     160     }
     161
     162     @Override
     163     public Bindings createBindings() {
     164         if (_global_per_engine) {
     165             // just create normal SimpleBindings.
     166             // We use same 'global' for all Bindings.
     167             return new SimpleBindings();
     168         }
     169         return createGlobalMirror();
     170     }
     171
     172     // Compilable methods
     173
     174     @Override
     175     public CompiledScript compile(final Reader reader) throws ScriptException {
     176         return asCompiledScript(makeSource(reader, context));
     177     }
     178
     179     @Override
     180     public CompiledScript compile(final String str) throws ScriptException {
     181         return asCompiledScript(makeSource(str, context));
     182     }
     183
     184     // Invocable methods
     185
     186     @Override
     187     public Object invokeFunction(final String name, final Object... args)
     188             throws ScriptException, NoSuchMethodException {
     189         return invokeImpl(null, name, args);
     190     }
     191
     192     @Override
     193     public Object invokeMethod(final Object thiz, final String name, final Object... args)
     194             throws ScriptException, NoSuchMethodException {
     195         if (thiz == null) {
     196             throw new IllegalArgumentException(getMessage("thiz.cannot.be.null"));
     197         }
     198         return invokeImpl(thiz, name, args);
     199     }
     200
     201     @Override
     202     public <T> T getInterface(final Class<T> clazz) {
     203         return getInterfaceInner(null, clazz);
     204     }
     205
     206     @Override
     207     public <T> T getInterface(final Object thiz, final Class<T> clazz) {
     208         if (thiz == null) {
     209             throw new IllegalArgumentException(getMessage("thiz.cannot.be.null"));
     210         }
     211         return getInterfaceInner(thiz, clazz);
     212     }
     213
     214     // Implementation only below this point
     215
     216     private static Source makeSource(final Reader reader, final ScriptContext ctxt) throws ScriptException {
     217         try {
     218             return sourceFor(getScriptName(ctxt), reader);
     219         } catch (final IOException e) {
     220             throw new ScriptException(e);
     221         }
     222     }
     223
     224     private static Source makeSource(final String src, final ScriptContext ctxt) {
     225         return sourceFor(getScriptName(ctxt), src);
     226     }
     227
     228     private static String getScriptName(final ScriptContext ctxt) {
     229         final Object val = ctxt.getAttribute(ScriptEngine.FILENAME);
     230         return (val != null) ? val.toString() : "<eval>";
     231     }
     232
     233     private <T> T getInterfaceInner(final Object thiz, final Class<T> clazz) {
     234         assert !(thiz instanceof ScriptObject) : "raw ScriptObject not expected here";
     235
     236         if (clazz == null || !clazz.isInterface()) {
     237             throw new IllegalArgumentException(getMessage("interface.class.expected"));
     238         }
     239
     240         // perform security access check as early as possible
     241         final SecurityManager sm = System.getSecurityManager();
     242         if (sm != null) {
     243             if (! Modifier.isPublic(clazz.getModifiers())) {
     244                 throw new SecurityException(getMessage("implementing.non.public.interface", clazz.getName()));
     245             }
     246             Context.checkPackageAccess(clazz);
     247         }
     248
     249         ScriptObject realSelf = null;
     250         Global realGlobal = null;
     251         if(thiz == null) {
     252             // making interface out of global functions
     253             realSelf = realGlobal = getNashornGlobalFrom(context);
     254         } else if (thiz instanceof ScriptObjectMirror) {
     255             final ScriptObjectMirror mirror = (ScriptObjectMirror)thiz;
     256             realSelf = mirror.getScriptObject();
     257             realGlobal = mirror.getHomeGlobal();
     258             if (! isOfContext(realGlobal, nashornContext)) {
     259                 throw new IllegalArgumentException(getMessage("script.object.from.another.engine"));
     260             }
     261         }
     262
     263         if (realSelf == null) {
     264             throw new IllegalArgumentException(getMessage("interface.on.non.script.object"));
     265         }
     266
     267         try {
     268             final Global oldGlobal = Context.getGlobal();
     269             final boolean globalChanged = (oldGlobal != realGlobal);
     270             try {
     271                 if (globalChanged) {
     272                     Context.setGlobal(realGlobal);
     273                 }
     274
     275                 if (! isInterfaceImplemented(clazz, realSelf)) {
     276                     return null;
     277                 }
     278                 return clazz.cast(JavaAdapterFactory.getConstructor(realSelf.getClass(), clazz,
     279                         MethodHandles.publicLookup()).invoke(realSelf));
     280             } finally {
     281                 if (globalChanged) {
     282                     Context.setGlobal(oldGlobal);
     283                 }
     284             }
     285         } catch(final RuntimeException|Error e) {
     286             throw e;
     287         } catch(final Throwable t) {
     288             throw new RuntimeException(t);
     289         }
     290     }
     291
     292     // Retrieve nashorn Global object for a given ScriptContext object
     293     private Global getNashornGlobalFrom(final ScriptContext ctxt) {
     294         if (_global_per_engine) {
     295             // shared single global object for all ENGINE_SCOPE Bindings
     296             return global;
     297         }
     298
     299         final Bindings bindings = ctxt.getBindings(ScriptContext.ENGINE_SCOPE);
     300         // is this Nashorn's own Bindings implementation?
     301         if (bindings instanceof ScriptObjectMirror) {
     302             final Global glob = globalFromMirror((ScriptObjectMirror)bindings);
     303             if (glob != null) {
     304                 return glob;
     305             }
     306         }
     307
     308         // Arbitrary user Bindings implementation. Look for NASHORN_GLOBAL in it!
     309         final Object scope = bindings.get(NASHORN_GLOBAL);
     310         if (scope instanceof ScriptObjectMirror) {
     311             final Global glob = globalFromMirror((ScriptObjectMirror)scope);
     312             if (glob != null) {
     313                 return glob;
     314             }
     315         }
     316
     317         // We didn't find associated nashorn global mirror in the Bindings given!
     318         // Create new global instance mirror and associate with the Bindings.
     319         final ScriptObjectMirror mirror = createGlobalMirror();
     320         bindings.put(NASHORN_GLOBAL, mirror);
     321         // Since we created this global explicitly for the non-default script context we set the
     322         // current script context in global permanently so that invokes work as expected. See JDK-8150219
     323         mirror.getHomeGlobal().setInitScriptContext(ctxt);
     324         return mirror.getHomeGlobal();
     325     }
     326
     327     // Retrieve nashorn Global object from a given ScriptObjectMirror
     328     private Global globalFromMirror(final ScriptObjectMirror mirror) {
     329         final ScriptObject sobj = mirror.getScriptObject();
     330         if (sobj instanceof Global && isOfContext((Global)sobj, nashornContext)) {
     331             return (Global)sobj;
     332         }
     333
     334         return null;
     335     }
     336
     337     // Create a new ScriptObjectMirror wrapping a newly created Nashorn Global object
     338     private ScriptObjectMirror createGlobalMirror() {
     339         final Global newGlobal = createNashornGlobal();
     340         return new ScriptObjectMirror(newGlobal, newGlobal);
     341     }
     342
     343     // Create a new Nashorn Global object
     344     private Global createNashornGlobal() {
     345         final Global newGlobal = AccessController.doPrivileged(new PrivilegedAction<Global>() {
     346             @Override
     347             public Global run() {
     348                 try {
     349                     return nashornContext.newGlobal();
     350                 } catch (final RuntimeException e) {
     351                     if (Context.DEBUG) {
     352                         e.printStackTrace();
     353                     }
     354                     throw e;
     355                 }
     356             }
     357         }, CREATE_GLOBAL_ACC_CTXT);
     358
     359         nashornContext.initGlobal(newGlobal, this);
     360
     361         return newGlobal;
     362     }
     363
     364     private Object invokeImpl(final Object selfObject, final String name, final Object... args) throws ScriptException, NoSuchMethodException {
     365         Objects.requireNonNull(name);
     366         assert !(selfObject instanceof ScriptObject) : "raw ScriptObject not expected here";
     367
     368         Global invokeGlobal = null;
     369         ScriptObjectMirror selfMirror = null;
     370         if (selfObject instanceof ScriptObjectMirror) {
     371             selfMirror = (ScriptObjectMirror)selfObject;
     372             if (! isOfContext(selfMirror.getHomeGlobal(), nashornContext)) {
     373                 throw new IllegalArgumentException(getMessage("script.object.from.another.engine"));
     374             }
     375             invokeGlobal = selfMirror.getHomeGlobal();
     376         } else if (selfObject == null) {
     377             // selfObject is null => global function call
     378             final Global ctxtGlobal = getNashornGlobalFrom(context);
     379             invokeGlobal = ctxtGlobal;
     380             selfMirror = (ScriptObjectMirror)ScriptObjectMirror.wrap(ctxtGlobal, ctxtGlobal);
     381         }
     382
     383         if (selfMirror != null) {
     384             try {
     385                 return ScriptObjectMirror.translateUndefined(selfMirror.callMember(name, args));
     386             } catch (final Exception e) {
     387                 final Throwable cause = e.getCause();
     388                 if (cause instanceof NoSuchMethodException) {
     389                     throw (NoSuchMethodException)cause;
     390                 }
     391                 throwAsScriptException(e, invokeGlobal);
     392                 throw new AssertionError("should not reach here");
     393             }
     394         }
     395
     396         // Non-script object passed as selfObject
     397         throw new IllegalArgumentException(getMessage("interface.on.non.script.object"));
     398     }
     399
     400     private Object evalImpl(final Source src, final ScriptContext ctxt) throws ScriptException {
     401         return evalImpl(compileImpl(src, ctxt), ctxt);
     402     }
     403
     404     private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt) throws ScriptException {
     405         return evalImpl(script, ctxt, getNashornGlobalFrom(ctxt));
     406     }
     407
     408     private Object evalImpl(final Context.MultiGlobalCompiledScript mgcs, final ScriptContext ctxt, final Global ctxtGlobal) throws ScriptException {
     409         final Global oldGlobal = Context.getGlobal();
     410         final boolean globalChanged = (oldGlobal != ctxtGlobal);
     411         try {
     412             if (globalChanged) {
     413                 Context.setGlobal(ctxtGlobal);
     414             }
     415
     416             final ScriptFunction script = mgcs.getFunction(ctxtGlobal);
     417             final ScriptContext oldCtxt = ctxtGlobal.getScriptContext();
     418             ctxtGlobal.setScriptContext(ctxt);
     419             try {
     420                 return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(ScriptRuntime.apply(script, ctxtGlobal), ctxtGlobal));
     421             } finally {
     422                 ctxtGlobal.setScriptContext(oldCtxt);
     423             }
     424         } catch (final Exception e) {
     425             throwAsScriptException(e, ctxtGlobal);
     426             throw new AssertionError("should not reach here");
     427         } finally {
     428             if (globalChanged) {
     429                 Context.setGlobal(oldGlobal);
     430             }
     431         }
     432     }
     433
     434     private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt, final Global ctxtGlobal) throws ScriptException {
     435         if (script == null) {
     436             return null;
     437         }
     438         final Global oldGlobal = Context.getGlobal();
     439         final boolean globalChanged = (oldGlobal != ctxtGlobal);
     440         try {
     441             if (globalChanged) {
     442                 Context.setGlobal(ctxtGlobal);
     443             }
     444
     445             final ScriptContext oldCtxt = ctxtGlobal.getScriptContext();
     446             ctxtGlobal.setScriptContext(ctxt);
     447             try {
     448                 return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(ScriptRuntime.apply(script, ctxtGlobal), ctxtGlobal));
     449             } finally {
     450                 ctxtGlobal.setScriptContext(oldCtxt);
     451             }
     452         } catch (final Exception e) {
     453             throwAsScriptException(e, ctxtGlobal);
     454             throw new AssertionError("should not reach here");
     455         } finally {
     456             if (globalChanged) {
     457                 Context.setGlobal(oldGlobal);
     458             }
     459         }
     460     }
     461
     462     private static void throwAsScriptException(final Exception e, final Global global) throws ScriptException {
     463         if (e instanceof ScriptException) {
     464             throw (ScriptException)e;
     465         } else if (e instanceof NashornException) {
     466             final NashornException ne = (NashornException)e;
     467             final ScriptException se = new ScriptException(
     468                 ne.getMessage(), ne.getFileName(),
     469                 ne.getLineNumber(), ne.getColumnNumber());
     470             ne.initEcmaError(global);
     471             se.initCause(e);
     472             throw se;
     473         } else if (e instanceof RuntimeException) {
     474             throw (RuntimeException)e;
     475         } else {
     476             // wrap any other exception as ScriptException
     477             throw new ScriptException(e);
     478         }
     479     }
     480
     481     private CompiledScript asCompiledScript(final Source source) throws ScriptException {
     482         final Context.MultiGlobalCompiledScript mgcs;
     483         final ScriptFunction func;
     484         final Global oldGlobal = Context.getGlobal();
     485         final Global newGlobal = getNashornGlobalFrom(context);
     486         final boolean globalChanged = (oldGlobal != newGlobal);
     487         try {
     488             if (globalChanged) {
     489                 Context.setGlobal(newGlobal);
     490             }
     491
     492             mgcs = nashornContext.compileScript(source);
     493             func = mgcs.getFunction(newGlobal);
     494         } catch (final Exception e) {
     495             throwAsScriptException(e, newGlobal);
     496             throw new AssertionError("should not reach here");
     497         } finally {
     498             if (globalChanged) {
     499                 Context.setGlobal(oldGlobal);
     500             }
     501         }
     502
     503         return new CompiledScript() {
     504             @Override
     505             public Object eval(final ScriptContext ctxt) throws ScriptException {
     506                 final Global globalObject = getNashornGlobalFrom(ctxt);
     507                 // Are we running the script in the same global in which it was compiled?
     508                 if (func.getScope() == globalObject) {
     509                     return evalImpl(func, ctxt, globalObject);
     510                 }
     511
     512                 // different global
     513                 return evalImpl(mgcs, ctxt, globalObject);
     514             }
     515             @Override
     516             public ScriptEngine getEngine() {
     517                 return NashornScriptEngine.this;
     518             }
     519         };
     520     }
     521
     522     private ScriptFunction compileImpl(final Source source, final ScriptContext ctxt) throws ScriptException {
     523         return compileImpl(source, getNashornGlobalFrom(ctxt));
     524     }
     525
     526     private ScriptFunction compileImpl(final Source source, final Global newGlobal) throws ScriptException {
     527         final Global oldGlobal = Context.getGlobal();
     528         final boolean globalChanged = (oldGlobal != newGlobal);
     529         try {
     530             if (globalChanged) {
     531                 Context.setGlobal(newGlobal);
     532             }
     533
     534             return nashornContext.compileScript(source, newGlobal);
     535         } catch (final Exception e) {
     536             throwAsScriptException(e, newGlobal);
     537             throw new AssertionError("should not reach here");
     538         } finally {
     539             if (globalChanged) {
     540                 Context.setGlobal(oldGlobal);
     541             }
     542         }
     543     }
     544
     545     private static boolean isInterfaceImplemented(final Class<?> iface, final ScriptObject sobj) {
     546         for (final Method method : iface.getMethods()) {
     547             // ignore methods of java.lang.Object class
     548             if (method.getDeclaringClass() == Object.class) {
     549                 continue;
     550             }
     551
     552             // skip check for default methods - non-abstract, interface methods
     553             if (! Modifier.isAbstract(method.getModifiers())) {
     554                 continue;
     555             }
     556
     557             final Object obj = sobj.get(method.getName());
     558             if (! (obj instanceof ScriptFunction)) {
     559                 return false;
     560             }
     561         }
     562         return true;
     563     }
     564
     565     private static boolean isOfContext(final Global global, final Context context) {
     566         return global.isOfContext(context);
     567     }
     568 }