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