Fri, 19 Apr 2013 16:11:16 +0200
8010701: Immutable nodes - final iteration
Reviewed-by: sundar, hannesw, jlaskey
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 */
26 package jdk.nashorn.api.scripting;
28 import static jdk.nashorn.internal.runtime.ECMAErrors.referenceError;
29 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.InputStreamReader;
34 import java.io.Reader;
35 import java.lang.reflect.Method;
36 import java.net.URL;
37 import java.security.AccessController;
38 import java.security.PrivilegedAction;
39 import java.security.PrivilegedActionException;
40 import java.security.PrivilegedExceptionAction;
41 import javax.script.AbstractScriptEngine;
42 import javax.script.Bindings;
43 import javax.script.Compilable;
44 import javax.script.CompiledScript;
45 import javax.script.Invocable;
46 import javax.script.ScriptContext;
47 import javax.script.ScriptEngine;
48 import javax.script.ScriptEngineFactory;
49 import javax.script.ScriptException;
50 import jdk.nashorn.internal.runtime.Context;
51 import jdk.nashorn.internal.runtime.ErrorManager;
52 import jdk.nashorn.internal.runtime.GlobalObject;
53 import jdk.nashorn.internal.runtime.Property;
54 import jdk.nashorn.internal.runtime.ScriptFunction;
55 import jdk.nashorn.internal.runtime.ScriptObject;
56 import jdk.nashorn.internal.runtime.ScriptRuntime;
57 import jdk.nashorn.internal.runtime.Source;
58 import jdk.nashorn.internal.runtime.linker.JavaAdapterFactory;
59 import jdk.nashorn.internal.runtime.options.Options;
61 /**
62 * JSR-223 compliant script engine for Nashorn. Instances are not created directly, but rather returned through
63 * {@link NashornScriptEngineFactory#getScriptEngine()}. Note that this engine implements the {@link Compilable} and
64 * {@link Invocable} interfaces, allowing for efficient precompilation and repeated execution of scripts.
65 * @see NashornScriptEngineFactory
66 */
68 public final class NashornScriptEngine extends AbstractScriptEngine implements Compilable, Invocable {
70 private final ScriptEngineFactory factory;
71 private final Context nashornContext;
72 private final ScriptObject global;
74 // default options passed to Nashorn Options object
75 private static final String[] DEFAULT_OPTIONS = new String[] { "-scripting", "-af", "-doe" };
77 NashornScriptEngine(final NashornScriptEngineFactory factory, final ClassLoader appLoader) {
78 this(factory, DEFAULT_OPTIONS, appLoader);
79 }
81 @SuppressWarnings("LeakingThisInConstructor")
82 NashornScriptEngine(final NashornScriptEngineFactory factory, final String[] args, final ClassLoader appLoader) {
83 this.factory = factory;
84 final Options options = new Options("nashorn");
85 options.process(args);
87 // throw ParseException on first error from script
88 final ErrorManager errMgr = new Context.ThrowErrorManager();
89 // create new Nashorn Context
90 this.nashornContext = AccessController.doPrivileged(new PrivilegedAction<Context>() {
91 @Override
92 public Context run() {
93 try {
94 return new Context(options, errMgr, appLoader);
95 } catch (final RuntimeException e) {
96 if (Context.DEBUG) {
97 e.printStackTrace();
98 }
99 throw e;
100 }
101 }
102 });
104 // create new global object
105 this.global = createNashornGlobal();
106 // set the default engine scope for the default context
107 context.setBindings(new ScriptObjectMirror(global, global), ScriptContext.ENGINE_SCOPE);
109 // evaluate engine initial script
110 try {
111 evalEngineScript();
112 } catch (final ScriptException e) {
113 if (Context.DEBUG) {
114 e.printStackTrace();
115 }
116 throw new RuntimeException(e);
117 }
118 }
120 @Override
121 public Object eval(final Reader reader, final ScriptContext ctxt) throws ScriptException {
122 try {
123 if (reader instanceof URLReader) {
124 final URL url = ((URLReader)reader).getURL();
125 return evalImpl(compileImpl(new Source(url.toString(), url), ctxt), ctxt);
126 }
127 return evalImpl(Source.readFully(reader), ctxt);
128 } catch (final IOException e) {
129 throw new ScriptException(e);
130 }
131 }
133 @Override
134 public Object eval(final String script, final ScriptContext ctxt) throws ScriptException {
135 return evalImpl(script.toCharArray(), ctxt);
136 }
138 @Override
139 public ScriptEngineFactory getFactory() {
140 return factory;
141 }
143 @Override
144 public Bindings createBindings() {
145 final ScriptObject newGlobal = createNashornGlobal();
146 return new ScriptObjectMirror(newGlobal, newGlobal);
147 }
149 // Compilable methods
151 @Override
152 public CompiledScript compile(final Reader reader) throws ScriptException {
153 try {
154 return asCompiledScript(compileImpl(Source.readFully(reader), context));
155 } catch (final IOException e) {
156 throw new ScriptException(e);
157 }
158 }
160 @Override
161 public CompiledScript compile(final String str) throws ScriptException {
162 return asCompiledScript(compileImpl(str.toCharArray(), context));
163 }
165 // Invocable methods
167 @Override
168 public Object invokeFunction(final String name, final Object... args)
169 throws ScriptException, NoSuchMethodException {
170 return invokeImpl(null, name, args);
171 }
173 @Override
174 public Object invokeMethod(final Object self, final String name, final Object... args)
175 throws ScriptException, NoSuchMethodException {
176 if (self == null) {
177 throw new IllegalArgumentException("script object can not be null");
178 }
179 return invokeImpl(self, name, args);
180 }
182 private <T> T getInterfaceInner(final Object self, final Class<T> clazz) {
183 final ScriptObject realSelf;
184 final ScriptObject ctxtGlobal = getNashornGlobalFrom(context);
185 if(self == null) {
186 realSelf = ctxtGlobal;
187 } else if (!(self instanceof ScriptObject)) {
188 realSelf = (ScriptObject)ScriptObjectMirror.unwrap(self, ctxtGlobal);
189 } else {
190 realSelf = (ScriptObject)self;
191 }
192 try {
193 final ScriptObject oldGlobal = getNashornGlobal();
194 try {
195 if(oldGlobal != ctxtGlobal) {
196 setNashornGlobal(ctxtGlobal);
197 }
199 if (! isInterfaceImplemented(clazz, realSelf)) {
200 return null;
201 }
202 return clazz.cast(JavaAdapterFactory.getConstructor(realSelf.getClass(), clazz).invoke(realSelf));
203 } finally {
204 if(oldGlobal != ctxtGlobal) {
205 setNashornGlobal(oldGlobal);
206 }
207 }
208 } catch(final RuntimeException|Error e) {
209 throw e;
210 } catch(final Throwable t) {
211 throw new RuntimeException(t);
212 }
213 }
215 @Override
216 public <T> T getInterface(final Class<T> clazz) {
217 return getInterfaceInner(null, clazz);
218 }
220 @Override
221 public <T> T getInterface(final Object self, final Class<T> clazz) {
222 if (self == null) {
223 throw new IllegalArgumentException("script object can not be null");
224 }
225 return getInterfaceInner(self, clazz);
226 }
228 // These are called from the "engine.js" script
230 /**
231 * This hook is used to search js global variables exposed from Java code.
232 *
233 * @param self 'this' passed from the script
234 * @param ctxt current ScriptContext in which name is searched
235 * @param name name of the variable searched
236 * @return the value of the named variable
237 */
238 public Object __noSuchProperty__(final Object self, final ScriptContext ctxt, final String name) {
239 final int scope = ctxt.getAttributesScope(name);
240 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
241 if (scope != -1) {
242 return ScriptObjectMirror.unwrap(ctxt.getAttribute(name, scope), ctxtGlobal);
243 }
245 if (self == UNDEFINED) {
246 // scope access and so throw ReferenceError
247 throw referenceError(ctxtGlobal, "not.defined", name);
248 }
250 return UNDEFINED;
251 }
253 private ScriptObject getNashornGlobalFrom(final ScriptContext ctxt) {
254 final Bindings bindings = ctxt.getBindings(ScriptContext.ENGINE_SCOPE);
255 if (bindings instanceof ScriptObjectMirror) {
256 ScriptObject sobj = ((ScriptObjectMirror)bindings).getScriptObject();
257 if (sobj instanceof GlobalObject) {
258 return sobj;
259 }
260 }
262 // didn't find global object from context given - return the engine-wide global
263 return global;
264 }
266 private ScriptObject createNashornGlobal() {
267 final ScriptObject newGlobal = AccessController.doPrivileged(new PrivilegedAction<ScriptObject>() {
268 @Override
269 public ScriptObject run() {
270 try {
271 return nashornContext.newGlobal();
272 } catch (final RuntimeException e) {
273 if (Context.DEBUG) {
274 e.printStackTrace();
275 }
276 throw e;
277 }
278 }
279 });
281 nashornContext.initGlobal(newGlobal);
283 // current ScriptContext exposed as "context"
284 newGlobal.addOwnProperty("context", Property.NOT_ENUMERABLE, UNDEFINED);
285 // current ScriptEngine instance exposed as "engine". We added @SuppressWarnings("LeakingThisInConstructor") as
286 // NetBeans identifies this assignment as such a leak - this is a false positive as we're setting this property
287 // in the Global of a Context we just created - both the Context and the Global were just created and can not be
288 // seen from another thread outside of this constructor.
289 newGlobal.addOwnProperty("engine", Property.NOT_ENUMERABLE, this);
290 // global script arguments with undefined value
291 newGlobal.addOwnProperty("arguments", Property.NOT_ENUMERABLE, UNDEFINED);
292 // file name default is null
293 newGlobal.addOwnProperty(ScriptEngine.FILENAME, Property.NOT_ENUMERABLE, null);
294 return newGlobal;
295 }
297 private void evalEngineScript() throws ScriptException {
298 evalSupportScript("resources/engine.js", NashornException.ENGINE_SCRIPT_SOURCE_NAME);
299 }
301 private void evalSupportScript(final String script, final String name) throws ScriptException {
302 try {
303 final InputStream is = AccessController.doPrivileged(
304 new PrivilegedExceptionAction<InputStream>() {
305 @Override
306 public InputStream run() throws Exception {
307 final URL url = NashornScriptEngine.class.getResource(script);
308 return url.openStream();
309 }
310 });
311 put(ScriptEngine.FILENAME, name);
312 try (final InputStreamReader isr = new InputStreamReader(is)) {
313 eval(isr);
314 }
315 } catch (final PrivilegedActionException | IOException e) {
316 throw new ScriptException(e);
317 } finally {
318 put(ScriptEngine.FILENAME, null);
319 }
320 }
322 // scripts should see "context" and "engine" as variables
323 private void setContextVariables(final ScriptContext ctxt) {
324 ctxt.setAttribute("context", ctxt, ScriptContext.ENGINE_SCOPE);
325 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
326 ctxtGlobal.set("context", ctxt, false);
327 Object args = ScriptObjectMirror.unwrap(ctxt.getAttribute("arguments"), ctxtGlobal);
328 if (args == null || args == UNDEFINED) {
329 args = ScriptRuntime.EMPTY_ARRAY;
330 }
331 // if no arguments passed, expose it
332 args = ((GlobalObject)ctxtGlobal).wrapAsObject(args);
333 ctxtGlobal.set("arguments", args, false);
334 }
336 private Object invokeImpl(final Object selfObject, final String name, final Object... args) throws ScriptException, NoSuchMethodException {
337 final ScriptObject oldGlobal = getNashornGlobal();
338 final ScriptObject ctxtGlobal = getNashornGlobalFrom(context);
339 final boolean globalChanged = (oldGlobal != ctxtGlobal);
341 Object self = globalChanged? ScriptObjectMirror.wrap(selfObject, oldGlobal) : selfObject;
343 try {
344 if (globalChanged) {
345 setNashornGlobal(ctxtGlobal);
346 }
348 ScriptObject sobj;
349 Object value = null;
351 self = ScriptObjectMirror.unwrap(self, ctxtGlobal);
353 // FIXME: should convert when self is not ScriptObject
354 if (self instanceof ScriptObject) {
355 sobj = (ScriptObject)self;
356 value = sobj.get(name);
357 } else if (self == null) {
358 self = ctxtGlobal;
359 sobj = ctxtGlobal;
360 value = sobj.get(name);
361 }
363 if (value instanceof ScriptFunction) {
364 final Object res;
365 try {
366 final Object[] modArgs = globalChanged? ScriptObjectMirror.wrapArray(args, oldGlobal) : args;
367 res = ScriptRuntime.checkAndApply((ScriptFunction)value, self, ScriptObjectMirror.unwrapArray(modArgs, ctxtGlobal));
368 } catch (final Exception e) {
369 throwAsScriptException(e);
370 throw new AssertionError("should not reach here");
371 }
372 return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(res, ctxtGlobal));
373 }
375 throw new NoSuchMethodException(name);
376 } finally {
377 if (globalChanged) {
378 setNashornGlobal(oldGlobal);
379 }
380 }
381 }
383 private Object evalImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException {
384 return evalImpl(compileImpl(buf, ctxt), ctxt);
385 }
387 private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt) throws ScriptException {
388 if (script == null) {
389 return null;
390 }
391 final ScriptObject oldGlobal = getNashornGlobal();
392 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
393 final boolean globalChanged = (oldGlobal != ctxtGlobal);
394 try {
395 if (globalChanged) {
396 setNashornGlobal(ctxtGlobal);
397 }
399 setContextVariables(ctxt);
400 return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(ScriptRuntime.apply(script, ctxtGlobal), ctxtGlobal));
401 } catch (final Exception e) {
402 throwAsScriptException(e);
403 throw new AssertionError("should not reach here");
404 } finally {
405 if (globalChanged) {
406 setNashornGlobal(oldGlobal);
407 }
408 }
409 }
411 private static void throwAsScriptException(final Exception e) throws ScriptException {
412 if (e instanceof ScriptException) {
413 throw (ScriptException)e;
414 } else if (e instanceof NashornException) {
415 final NashornException ne = (NashornException)e;
416 final ScriptException se = new ScriptException(
417 ne.getMessage(), ne.getFileName(),
418 ne.getLineNumber(), ne.getColumnNumber());
419 se.initCause(e);
420 throw se;
421 } else if (e instanceof RuntimeException) {
422 throw (RuntimeException)e;
423 } else {
424 // wrap any other exception as ScriptException
425 throw new ScriptException(e);
426 }
427 }
429 private CompiledScript asCompiledScript(final ScriptFunction script) {
430 return new CompiledScript() {
431 @Override
432 public Object eval(final ScriptContext ctxt) throws ScriptException {
433 return evalImpl(script, ctxt);
434 }
435 @Override
436 public ScriptEngine getEngine() {
437 return NashornScriptEngine.this;
438 }
439 };
440 }
442 private ScriptFunction compileImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException {
443 final Object val = ctxt.getAttribute(ScriptEngine.FILENAME);
444 final String fileName = (val != null) ? val.toString() : "<eval>";
445 return compileImpl(new Source(fileName, buf), ctxt);
446 }
448 private ScriptFunction compileImpl(final Source source, final ScriptContext ctxt) throws ScriptException {
449 final ScriptObject oldGlobal = getNashornGlobal();
450 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
451 final boolean globalChanged = (oldGlobal != ctxtGlobal);
452 try {
453 if (globalChanged) {
454 setNashornGlobal(ctxtGlobal);
455 }
457 return nashornContext.compileScript(source, ctxtGlobal);
458 } catch (final Exception e) {
459 throwAsScriptException(e);
460 throw new AssertionError("should not reach here");
461 } finally {
462 if (globalChanged) {
463 setNashornGlobal(oldGlobal);
464 }
465 }
466 }
468 private static boolean isInterfaceImplemented(final Class<?> iface, final ScriptObject sobj) {
469 for (final Method method : iface.getMethods()) {
470 // ignore methods of java.lang.Object class
471 if (method.getDeclaringClass() == Object.class) {
472 continue;
473 }
475 Object obj = sobj.get(method.getName());
476 if (! (obj instanceof ScriptFunction)) {
477 return false;
478 }
479 }
480 return true;
481 }
483 // don't make this public!!
484 static ScriptObject getNashornGlobal() {
485 return Context.getGlobal();
486 }
488 static void setNashornGlobal(final ScriptObject newGlobal) {
489 AccessController.doPrivileged(new PrivilegedAction<Void>() {
490 @Override
491 public Void run() {
492 Context.setGlobal(newGlobal);
493 return null;
494 }
495 });
496 }
497 }