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