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