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