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.ECMAErrors.typeError; 30 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED; 31 import static jdk.nashorn.internal.runtime.linker.Lookup.MH; 32 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.InputStreamReader; 36 import java.io.Reader; 37 import java.lang.invoke.MethodHandle; 38 import java.lang.reflect.Method; 39 import java.lang.reflect.Modifier; 40 import java.net.URL; 41 import java.security.AccessController; 42 import java.security.PrivilegedAction; 43 import java.security.PrivilegedActionException; 44 import java.security.PrivilegedExceptionAction; 45 import javax.script.AbstractScriptEngine; 46 import javax.script.Bindings; 47 import javax.script.Compilable; 48 import javax.script.CompiledScript; 49 import javax.script.Invocable; 50 import javax.script.ScriptContext; 51 import javax.script.ScriptEngine; 52 import javax.script.ScriptEngineFactory; 53 import javax.script.ScriptException; 54 import javax.script.SimpleBindings; 55 import jdk.nashorn.internal.runtime.Context; 56 import jdk.nashorn.internal.runtime.ErrorManager; 57 import jdk.nashorn.internal.runtime.GlobalObject; 58 import jdk.nashorn.internal.runtime.Property; 59 import jdk.nashorn.internal.runtime.ScriptFunction; 60 import jdk.nashorn.internal.runtime.ScriptObject; 61 import jdk.nashorn.internal.runtime.ScriptRuntime; 62 import jdk.nashorn.internal.runtime.Source; 63 import jdk.nashorn.internal.runtime.linker.JavaAdapterFactory; 64 import jdk.nashorn.internal.runtime.options.Options; 65 66 /** 67 * JSR-223 compliant script engine for Nashorn. Instances are not created directly, but rather returned through 68 * {@link NashornScriptEngineFactory#getScriptEngine()}. Note that this engine implements the {@link Compilable} and 69 * {@link Invocable} interfaces, allowing for efficient precompilation and repeated execution of scripts. 70 * @see NashornScriptEngineFactory 71 */ 72 73 public final class NashornScriptEngine extends AbstractScriptEngine implements Compilable, Invocable { 74 75 private final ScriptEngineFactory factory; 76 private final Context nashornContext; 77 private final ScriptObject global; 78 79 // default options passed to Nashorn Options object 80 private static final String[] DEFAULT_OPTIONS = new String[] { "-scripting", "-af", "-doe" }; 81 82 NashornScriptEngine(final NashornScriptEngineFactory factory) { 83 this(factory, DEFAULT_OPTIONS); 84 } 85 86 @SuppressWarnings("LeakingThisInConstructor") 87 NashornScriptEngine(final NashornScriptEngineFactory factory, final String[] args) { 88 this.factory = factory; 89 final Options options = new Options("nashorn"); 90 options.process(args); 91 92 // throw ParseException on first error from script 93 final ErrorManager errors = new Context.ThrowErrorManager(); 94 // create new Nashorn Context and get global object 95 this.nashornContext = AccessController.doPrivileged(new PrivilegedAction<Context>() { 96 @Override 97 public Context run() { 98 try { 99 return new Context(options, errors); 100 } catch (final RuntimeException e) { 101 if (Context.DEBUG) { 102 e.printStackTrace(); 103 } 104 throw e; 105 } 106 } 107 }); 108 109 // create new global object 110 this.global = nashornContext.createGlobal(); 111 112 // current ScriptContext exposed as "context" 113 global.addOwnProperty("context", Property.NOT_ENUMERABLE, context); 114 // current ScriptEngine instance exposed as "engine". We added @SuppressWarnings("LeakingThisInConstructor") as 115 // NetBeans identifies this assignment as such a leak - this is a false positive as we're setting this property 116 // in the Global of a Context we just created - both the Context and the Global were just created and can not be 117 // seen from another thread outside of this constructor. 118 global.addOwnProperty("engine", Property.NOT_ENUMERABLE, this); 119 // global script arguments 120 global.addOwnProperty("arguments", Property.NOT_ENUMERABLE, UNDEFINED); 121 122 // evaluate engine initial script 123 try { 124 AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() { 125 @Override 126 public Void run() throws ScriptException { 127 evalEngineScript(); 128 return null; 129 } 130 }); 131 } catch (final PrivilegedActionException e) { 132 if (Context.DEBUG) { 133 e.printStackTrace(); 134 } 135 throw new RuntimeException(e); 136 } 137 } 138 139 @Override 140 public Object eval(final Reader reader, final ScriptContext ctxt) throws ScriptException { 141 try { 142 return evalImpl(Source.readFully(reader), ctxt); 143 } catch (final IOException e) { 144 throw new ScriptException(e); 145 } 146 } 147 148 @Override 149 public Object eval(final String script, final ScriptContext ctxt) throws ScriptException { 150 return evalImpl(script.toCharArray(), ctxt); 151 } 152 153 /* 154 * FIXME: This is not exactly right. Script execution should actually 155 * put the variables in ENGINE_SCOPE bindings. But, it is difficult 156 * (not possible?) with the way ScriptObject is implemented. So, for now 157 * giving access to script variables by accessing it from "globals". This 158 * way at least engine.get("foo") will work whenever "foo" is a global var 159 * defined by eval'ed scripts. 160 */ 161 @Override 162 public Object get(final String key) { 163 Object value = super.get(key); 164 if (value == null) { 165 value = ScriptObjectMirror.wrap(global.get(key), global); 166 } 167 return (value == UNDEFINED) ? null : value; 168 } 169 170 @Override 171 public ScriptEngineFactory getFactory() { 172 return factory; 173 } 174 175 @Override 176 public Bindings createBindings() { 177 return new SimpleBindings(); 178 } 179 180 // Compilable methods 181 182 @Override 183 public CompiledScript compile(final Reader reader) throws ScriptException { 184 try { 185 return asCompiledScript(compileImpl(Source.readFully(reader), context)); 186 } catch (final IOException e) { 187 throw new ScriptException(e); 188 } 189 } 190 191 @Override 192 public CompiledScript compile(final String str) throws ScriptException { 193 return asCompiledScript(compileImpl(str.toCharArray(), context)); 194 } 195 196 // Invocable methods 197 198 @Override 199 public Object invokeFunction(final String name, final Object... args) 200 throws ScriptException, NoSuchMethodException { 201 return invokeImpl(null, name, args); 202 } 203 204 @Override 205 public Object invokeMethod(final Object self, final String name, final Object... args) 206 throws ScriptException, NoSuchMethodException { 207 if (self == null) { 208 throw new IllegalArgumentException("script object can not be null"); 209 } 210 return invokeImpl(self, name, args); 211 } 212 213 private <T> T getInterfaceInner(final Object self, final Class<T> clazz) { 214 final Object realSelf; 215 if(self == null) { 216 realSelf = global; 217 } else if (!(self instanceof ScriptObject)) { 218 realSelf = ScriptObjectMirror.unwrap(self, global); 219 } else { 220 realSelf = self; 221 } 222 try { 223 final ScriptObject oldGlobal = Context.getGlobal(); 224 try { 225 if(oldGlobal != global) { 226 setNashornGlobal(global); 227 } 228 return clazz.cast(JavaAdapterFactory.getConstructor(realSelf.getClass(), clazz).invoke(realSelf)); 229 } finally { 230 if(oldGlobal != global) { 231 setNashornGlobal(oldGlobal); 232 } 233 } 234 } catch(final RuntimeException|Error e) { 235 throw e; 236 } catch(final Throwable t) { 237 throw new RuntimeException(t); 238 } 239 } 240 241 @Override 242 public <T> T getInterface(final Class<T> clazz) { 243 return getInterfaceInner(null, clazz); 244 } 245 246 @Override 247 public <T> T getInterface(final Object self, final Class<T> clazz) { 248 if (self == null) { 249 throw new IllegalArgumentException("script object can not be null"); 250 } 251 return getInterfaceInner(self, clazz); 252 } 253 254 // These are called from the "engine.js" script 255 256 /** 257 * This hook is used to search js global variables exposed from Java code. 258 * 259 * @param self 'this' passed from the script 260 * @param ctxt current ScriptContext in which name is searched 261 * @param name name of the variable searched 262 * @return the value of the named variable 263 */ 264 public Object __noSuchProperty__(final Object self, final ScriptContext ctxt, final String name) { 265 final int scope = ctxt.getAttributesScope(name); 266 if (scope != -1) { 267 return ScriptObjectMirror.unwrap(ctxt.getAttribute(name, scope), global); 268 } 269 270 if (self == UNDEFINED) { 271 // scope access and so throw ReferenceError 272 referenceError(global, "not.defined", name); 273 } 274 275 return UNDEFINED; 276 } 277 278 /** 279 * This hook is used to call js global functions exposed from Java code. 280 * 281 * @param self 'this' passed from the script 282 * @param ctxt current ScriptContext in which method is searched 283 * @param name name of the method 284 * @param args arguments to be passed to the method 285 * @return return value of the called method 286 */ 287 public Object __noSuchMethod__(final Object self, final ScriptContext ctxt, final String name, final Object args) { 288 final int scope = ctxt.getAttributesScope(name); 289 Object value; 290 291 if (scope != -1) { 292 value = ctxt.getAttribute(name, scope); 293 } else { 294 if (self == UNDEFINED) { 295 referenceError(global, "not.defined", name); 296 } else { 297 typeError(global, "no.such.function", name, ScriptRuntime.safeToString(global)); 298 } 299 return UNDEFINED; 300 } 301 302 value = ScriptObjectMirror.unwrap(value, global); 303 304 if (value instanceof Method) { 305 final Method method = (Method) value; 306 final int mods = method.getModifiers(); 307 if (Modifier.isStatic(mods) && Modifier.isPublic(mods)) { 308 value = MH.find((Method)value); 309 } 310 } 311 312 if (value instanceof MethodHandle) { 313 value = ((GlobalObject)global).newScriptFunction(name, (MethodHandle)value, null, false); 314 } 315 316 if (!(value instanceof ScriptFunction)) { 317 typeError(global, "not.a.function", name); 318 } 319 320 if (value instanceof ScriptFunction) { 321 return ScriptObjectMirror.unwrap(ScriptRuntime.apply((ScriptFunction)value, global, args), global); 322 } 323 324 typeError(global, "not.a.function", ScriptRuntime.safeToString(name)); 325 326 return UNDEFINED; 327 } 328 329 private void evalEngineScript() throws ScriptException { 330 evalSupportScript("resources/engine.js"); 331 } 332 333 private void evalSupportScript(String script) throws ScriptException { 334 final URL url = NashornScriptEngine.class.getResource(script); 335 try { 336 final InputStream is = url.openStream(); 337 put(ScriptEngine.FILENAME, url); 338 try (final InputStreamReader isr = new InputStreamReader(is)) { 339 eval(isr); 340 } 341 } catch (final IOException e) { 342 throw new ScriptException(e); 343 } finally { 344 put(ScriptEngine.FILENAME, null); 345 } 346 } 347 348 // scripts should see "context" and "engine" as variables 349 private void setContextVariables(final ScriptContext ctxt) { 350 ctxt.setAttribute("context", ctxt, ScriptContext.ENGINE_SCOPE); 351 // current ScriptContext exposed as "context" 352 global.set("context", ctxt, false); 353 Object args = ctxt.getAttribute("arguments"); 354 // if no arguments passed, make it empty array 355 if (args == null) { 356 args = ScriptRuntime.EMPTY_ARRAY; 357 } 358 args = ((GlobalObject)global).wrapAsObject(args); 359 global.set("arguments", args, false); 360 } 361 362 private Object invokeImpl(final Object selfObject, final String name, final Object... args) throws ScriptException, NoSuchMethodException { 363 final ScriptObject oldGlobal = Context.getGlobal(); 364 final boolean globalChanged = (oldGlobal != global); 365 366 Object self = selfObject; 367 368 try { 369 if (globalChanged) { 370 setNashornGlobal(global); 371 } 372 373 ScriptObject sobj; 374 Object value; 375 376 self = ScriptObjectMirror.unwrap(self, global); 377 378 // FIXME: should convert when self is not ScriptObject 379 if (self instanceof ScriptObject) { 380 sobj = (ScriptObject)self; 381 value = sobj.get(name); 382 } else if (self == null) { 383 self = global; 384 sobj = global; 385 value = sobj.get(name); 386 } else { 387 // Find the java method and make a ScriptFunction of it. 388 final Method[] methods = self.getClass().getMethods(); 389 Method target = null; 390 391 for (final Method mth : methods) { 392 // choose the right overload by number of arguments -- don't 393 // care overload resolution for now.. 394 if (mth.getName().equals(name) && 395 mth.getParameterTypes().length == args.length) { 396 target = mth; 397 break; 398 } 399 } 400 if (target == null) { 401 throw new NoSuchMethodException(name); 402 } 403 final GlobalObject gobj = (GlobalObject) global; 404 value = gobj.newScriptFunction(name, MH.find(target), null, false); 405 } 406 407 if (value instanceof ScriptFunction) { 408 final Object res; 409 try { 410 res = ScriptRuntime.apply((ScriptFunction)value, self, ScriptObjectMirror.unwrapArray(args, global)); 411 } catch (final Exception e) { 412 throwAsScriptException(e); 413 throw new AssertionError("should not reach here"); 414 } 415 return ScriptObjectMirror.wrap(res, global); 416 } 417 418 throw new NoSuchMethodException(name); 419 } finally { 420 if (globalChanged) { 421 setNashornGlobal(oldGlobal); 422 } 423 } 424 } 425 426 private Object evalImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException { 427 return evalImpl(compileImpl(buf, ctxt), ctxt); 428 } 429 430 private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt) throws ScriptException { 431 if (script == null) { 432 return null; 433 } 434 final ScriptObject oldGlobal = Context.getGlobal(); 435 final boolean globalChanged = (oldGlobal != global); 436 try { 437 if (globalChanged) { 438 setNashornGlobal(global); 439 } 440 441 setContextVariables(ctxt); 442 final Object val = ctxt.getAttribute(ScriptEngine.FILENAME); 443 final String fileName = (val != null) ? val.toString() : "<eval>"; 444 445 // NOTE: FIXME: If this is jrunscript's init.js, we want to run the replacement. 446 // This should go away once we fix jrunscript's copy of init.js. 447 if ("<system-init>".equals(fileName)) { 448 evalSupportScript("resources/init.js"); 449 return null; 450 } 451 452 Object res = ScriptRuntime.apply(script, global); 453 res = ScriptObjectMirror.wrap(res, global); 454 return (res == UNDEFINED) ? null : res; 455 } catch (final Exception e) { 456 throwAsScriptException(e); 457 throw new AssertionError("should not reach here"); 458 } finally { 459 if (globalChanged) { 460 setNashornGlobal(oldGlobal); 461 } 462 } 463 } 464 465 private static void throwAsScriptException(final Exception e) throws ScriptException { 466 if (e instanceof ScriptException) { 467 throw (ScriptException)e; 468 } else if (e instanceof NashornException) { 469 final NashornException ne = (NashornException)e; 470 final ScriptException se = new ScriptException( 471 ne.getMessage(), ne.getFileName(), 472 ne.getLineNumber(), ne.getColumnNumber()); 473 se.initCause(e); 474 throw se; 475 } else if (e instanceof RuntimeException) { 476 throw (RuntimeException)e; 477 } else { 478 // wrap any other exception as ScriptException 479 throw new ScriptException(e); 480 } 481 } 482 483 private CompiledScript asCompiledScript(final ScriptFunction script) { 484 return new CompiledScript() { 485 @Override 486 public Object eval(final ScriptContext ctxt) throws ScriptException { 487 return evalImpl(script, ctxt); 488 } 489 @Override 490 public ScriptEngine getEngine() { 491 return NashornScriptEngine.this; 492 } 493 }; 494 } 495 496 private ScriptFunction compileImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException { 497 final ScriptObject oldGlobal = Context.getGlobal(); 498 final boolean globalChanged = (oldGlobal != global); 499 try { 500 final Object val = ctxt.getAttribute(ScriptEngine.FILENAME); 501 final String fileName = (val != null) ? val.toString() : "<eval>"; 502 503 final Source source = new Source(fileName, buf); 504 if (globalChanged) { 505 setNashornGlobal(global); 506 } 507 508 setContextVariables(ctxt); 509 return nashornContext.compileScript(source, global, nashornContext._strict); 510 } catch (final Exception e) { 511 throwAsScriptException(e); 512 throw new AssertionError("should not reach here"); 513 } finally { 514 if (globalChanged) { 515 setNashornGlobal(oldGlobal); 516 } 517 } 518 } 519 520 // don't make this public!! 521 static void setNashornGlobal(final ScriptObject global) { 522 AccessController.doPrivileged(new PrivilegedAction<Void>() { 523 @Override 524 public Void run() { 525 Context.setGlobal(global); 526 return null; 527 } 528 }); 529 } 530 }