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