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