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