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