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.lang.reflect.Modifier; 37 import java.net.URL; 38 import java.nio.charset.Charset; 39 import java.security.AccessControlContext; 40 import java.security.AccessController; 41 import java.security.Permissions; 42 import java.security.PrivilegedAction; 43 import java.security.PrivilegedActionException; 44 import java.security.PrivilegedExceptionAction; 45 import java.security.ProtectionDomain; 46 import java.text.MessageFormat; 47 import java.util.Locale; 48 import java.util.ResourceBundle; 49 import javax.script.AbstractScriptEngine; 50 import javax.script.Bindings; 51 import javax.script.Compilable; 52 import javax.script.CompiledScript; 53 import javax.script.Invocable; 54 import javax.script.ScriptContext; 55 import javax.script.ScriptEngine; 56 import javax.script.ScriptEngineFactory; 57 import javax.script.ScriptException; 58 import jdk.nashorn.internal.runtime.Context; 59 import jdk.nashorn.internal.runtime.ErrorManager; 60 import jdk.nashorn.internal.runtime.GlobalObject; 61 import jdk.nashorn.internal.runtime.Property; 62 import jdk.nashorn.internal.runtime.ScriptFunction; 63 import jdk.nashorn.internal.runtime.ScriptObject; 64 import jdk.nashorn.internal.runtime.ScriptRuntime; 65 import jdk.nashorn.internal.runtime.Source; 66 import jdk.nashorn.internal.runtime.linker.JavaAdapterFactory; 67 import jdk.nashorn.internal.runtime.options.Options; 68 69 /** 70 * JSR-223 compliant script engine for Nashorn. Instances are not created directly, but rather returned through 71 * {@link NashornScriptEngineFactory#getScriptEngine()}. Note that this engine implements the {@link Compilable} and 72 * {@link Invocable} interfaces, allowing for efficient precompilation and repeated execution of scripts. 73 * @see NashornScriptEngineFactory 74 */ 75 76 public final class NashornScriptEngine extends AbstractScriptEngine implements Compilable, Invocable { 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 private final ScriptEngineFactory factory; 87 private final Context nashornContext; 88 private final ScriptObject global; 89 // initialized bit late to be made 'final'. Property object for "context" 90 // property of global object 91 private Property contextProperty; 92 93 // default options passed to Nashorn Options object 94 private static final String[] DEFAULT_OPTIONS = new String[] { "-scripting", "-doe" }; 95 96 private static final String MESSAGES_RESOURCE = "jdk.nashorn.api.scripting.resources.Messages"; 97 98 private static final ResourceBundle MESSAGES_BUNDLE; 99 static { 100 MESSAGES_BUNDLE = ResourceBundle.getBundle(MESSAGES_RESOURCE, Locale.getDefault()); 101 } 102 103 private static String getMessage(final String msgId, final String... args) { 104 try { 105 return new MessageFormat(MESSAGES_BUNDLE.getString(msgId)).format(args); 106 } catch (final java.util.MissingResourceException e) { 107 throw new RuntimeException("no message resource found for message id: "+ msgId); 108 } 109 } 110 111 NashornScriptEngine(final NashornScriptEngineFactory factory, final ClassLoader appLoader) { 112 this(factory, DEFAULT_OPTIONS, appLoader); 113 } 114 115 NashornScriptEngine(final NashornScriptEngineFactory factory, final String[] args, final ClassLoader appLoader) { 116 this.factory = factory; 117 final Options options = new Options("nashorn"); 118 options.process(args); 119 120 // throw ParseException on first error from script 121 final ErrorManager errMgr = new Context.ThrowErrorManager(); 122 // create new Nashorn Context 123 this.nashornContext = AccessController.doPrivileged(new PrivilegedAction<Context>() { 124 @Override 125 public Context run() { 126 try { 127 return new Context(options, errMgr, appLoader); 128 } catch (final RuntimeException e) { 129 if (Context.DEBUG) { 130 e.printStackTrace(); 131 } 132 throw e; 133 } 134 } 135 }, CREATE_CONTEXT_ACC_CTXT); 136 137 // create new global object 138 this.global = createNashornGlobal(); 139 // set the default engine scope for the default context 140 context.setBindings(new ScriptObjectMirror(global, global), ScriptContext.ENGINE_SCOPE); 141 142 // evaluate engine initial script 143 try { 144 evalEngineScript(); 145 } catch (final ScriptException e) { 146 if (Context.DEBUG) { 147 e.printStackTrace(); 148 } 149 throw new RuntimeException(e); 150 } 151 } 152 153 @Override 154 public Object eval(final Reader reader, final ScriptContext ctxt) throws ScriptException { 155 try { 156 if (reader instanceof URLReader) { 157 final URL url = ((URLReader)reader).getURL(); 158 final Charset cs = ((URLReader)reader).getCharset(); 159 return evalImpl(compileImpl(new Source(url.toString(), url, cs), ctxt), ctxt); 160 } 161 return evalImpl(Source.readFully(reader), ctxt); 162 } catch (final IOException e) { 163 throw new ScriptException(e); 164 } 165 } 166 167 @Override 168 public Object eval(final String script, final ScriptContext ctxt) throws ScriptException { 169 return evalImpl(script.toCharArray(), ctxt); 170 } 171 172 @Override 173 public ScriptEngineFactory getFactory() { 174 return factory; 175 } 176 177 @Override 178 public Bindings createBindings() { 179 final ScriptObject newGlobal = createNashornGlobal(); 180 return new ScriptObjectMirror(newGlobal, newGlobal); 181 } 182 183 // Compilable methods 184 185 @Override 186 public CompiledScript compile(final Reader reader) throws ScriptException { 187 try { 188 return asCompiledScript(compileImpl(Source.readFully(reader), context)); 189 } catch (final IOException e) { 190 throw new ScriptException(e); 191 } 192 } 193 194 @Override 195 public CompiledScript compile(final String str) throws ScriptException { 196 return asCompiledScript(compileImpl(str.toCharArray(), context)); 197 } 198 199 // Invocable methods 200 201 @Override 202 public Object invokeFunction(final String name, final Object... args) 203 throws ScriptException, NoSuchMethodException { 204 return invokeImpl(null, name, args); 205 } 206 207 @Override 208 public Object invokeMethod(final Object thiz, final String name, final Object... args) 209 throws ScriptException, NoSuchMethodException { 210 if (thiz == null) { 211 throw new IllegalArgumentException(getMessage("thiz.cannot.be.null")); 212 } 213 return invokeImpl(thiz, name, args); 214 } 215 216 private <T> T getInterfaceInner(final Object thiz, final Class<T> clazz) { 217 if (clazz == null || !clazz.isInterface()) { 218 throw new IllegalArgumentException(getMessage("interface.class.expected")); 219 } 220 221 // perform security access check as early as possible 222 final SecurityManager sm = System.getSecurityManager(); 223 if (sm != null) { 224 if (! Modifier.isPublic(clazz.getModifiers())) { 225 throw new SecurityException(getMessage("implementing.non.public.interface", clazz.getName())); 226 } 227 Context.checkPackageAccess(clazz.getName()); 228 } 229 230 ScriptObject realSelf = null; 231 ScriptObject realGlobal = null; 232 if(thiz == null) { 233 // making interface out of global functions 234 realSelf = realGlobal = getNashornGlobalFrom(context); 235 } else if (thiz instanceof ScriptObjectMirror) { 236 final ScriptObjectMirror mirror = (ScriptObjectMirror)thiz; 237 realSelf = mirror.getScriptObject(); 238 realGlobal = mirror.getHomeGlobal(); 239 if (! realGlobal.isOfContext(nashornContext)) { 240 throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); 241 } 242 } else if (thiz instanceof ScriptObject) { 243 // called from script code. 244 realSelf = (ScriptObject)thiz; 245 realGlobal = Context.getGlobal(); 246 if (realGlobal == null) { 247 throw new IllegalArgumentException(getMessage("no.current.nashorn.global")); 248 } 249 250 if (! realGlobal.isOfContext(nashornContext)) { 251 throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); 252 } 253 } 254 255 if (realSelf == null) { 256 throw new IllegalArgumentException(getMessage("interface.on.non.script.object")); 257 } 258 259 try { 260 final ScriptObject oldGlobal = Context.getGlobal(); 261 final boolean globalChanged = (oldGlobal != realGlobal); 262 try { 263 if (globalChanged) { 264 Context.setGlobal(realGlobal); 265 } 266 267 if (! isInterfaceImplemented(clazz, realSelf)) { 268 return null; 269 } 270 return clazz.cast(JavaAdapterFactory.getConstructor(realSelf.getClass(), clazz).invoke(realSelf)); 271 } finally { 272 if (globalChanged) { 273 Context.setGlobal(oldGlobal); 274 } 275 } 276 } catch(final RuntimeException|Error e) { 277 throw e; 278 } catch(final Throwable t) { 279 throw new RuntimeException(t); 280 } 281 } 282 283 @Override 284 public <T> T getInterface(final Class<T> clazz) { 285 return getInterfaceInner(null, clazz); 286 } 287 288 @Override 289 public <T> T getInterface(final Object thiz, final Class<T> clazz) { 290 if (thiz == null) { 291 throw new IllegalArgumentException(getMessage("thiz.cannot.be.null")); 292 } 293 return getInterfaceInner(thiz, clazz); 294 } 295 296 // These are called from the "engine.js" script 297 298 /** 299 * This hook is used to search js global variables exposed from Java code. 300 * 301 * @param self 'this' passed from the script 302 * @param ctxt current ScriptContext in which name is searched 303 * @param name name of the variable searched 304 * @return the value of the named variable 305 */ 306 public Object __noSuchProperty__(final Object self, final ScriptContext ctxt, final String name) { 307 final int scope = ctxt.getAttributesScope(name); 308 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt); 309 if (scope != -1) { 310 return ScriptObjectMirror.unwrap(ctxt.getAttribute(name, scope), ctxtGlobal); 311 } 312 313 if (self == UNDEFINED) { 314 // scope access and so throw ReferenceError 315 throw referenceError(ctxtGlobal, "not.defined", name); 316 } 317 318 return UNDEFINED; 319 } 320 321 private ScriptObject getNashornGlobalFrom(final ScriptContext ctxt) { 322 final Bindings bindings = ctxt.getBindings(ScriptContext.ENGINE_SCOPE); 323 if (bindings instanceof ScriptObjectMirror) { 324 final ScriptObjectMirror mirror = (ScriptObjectMirror)bindings; 325 ScriptObject sobj = ((ScriptObjectMirror)bindings).getScriptObject(); 326 if (sobj instanceof GlobalObject && sobj.isOfContext(nashornContext)) { 327 return sobj; 328 } 329 } 330 331 // didn't find global object from context given - return the engine-wide global 332 return global; 333 } 334 335 private ScriptObject createNashornGlobal() { 336 final ScriptObject newGlobal = AccessController.doPrivileged(new PrivilegedAction<ScriptObject>() { 337 @Override 338 public ScriptObject run() { 339 try { 340 return nashornContext.newGlobal(); 341 } catch (final RuntimeException e) { 342 if (Context.DEBUG) { 343 e.printStackTrace(); 344 } 345 throw e; 346 } 347 } 348 }, CREATE_GLOBAL_ACC_CTXT); 349 350 nashornContext.initGlobal(newGlobal); 351 352 final int NON_ENUMERABLE_CONSTANT = Property.NOT_ENUMERABLE | Property.NOT_CONFIGURABLE | Property.NOT_WRITABLE; 353 // current ScriptContext exposed as "context" 354 // "context" is non-writable from script - but script engine still 355 // needs to set it and so save the context Property object 356 contextProperty = newGlobal.addOwnProperty("context", NON_ENUMERABLE_CONSTANT, UNDEFINED); 357 // current ScriptEngine instance exposed as "engine". We added @SuppressWarnings("LeakingThisInConstructor") as 358 // NetBeans identifies this assignment as such a leak - this is a false positive as we're setting this property 359 // in the Global of a Context we just created - both the Context and the Global were just created and can not be 360 // seen from another thread outside of this constructor. 361 newGlobal.addOwnProperty("engine", NON_ENUMERABLE_CONSTANT, this); 362 // global script arguments with undefined value 363 newGlobal.addOwnProperty("arguments", Property.NOT_ENUMERABLE, UNDEFINED); 364 // file name default is null 365 newGlobal.addOwnProperty(ScriptEngine.FILENAME, Property.NOT_ENUMERABLE, null); 366 return newGlobal; 367 } 368 369 private void evalEngineScript() throws ScriptException { 370 final String script = "resources/engine.js"; 371 final String name = NashornException.ENGINE_SCRIPT_SOURCE_NAME; 372 try { 373 final InputStream is = AccessController.doPrivileged( 374 new PrivilegedExceptionAction<InputStream>() { 375 @Override 376 public InputStream run() throws Exception { 377 final URL url = NashornScriptEngine.class.getResource(script); 378 return url.openStream(); 379 } 380 }); 381 put(ScriptEngine.FILENAME, name); 382 try (final InputStreamReader isr = new InputStreamReader(is)) { 383 eval(isr); 384 } 385 } catch (final PrivilegedActionException | IOException e) { 386 if (Context.DEBUG) { 387 e.printStackTrace(); 388 } 389 throw new ScriptException(e); 390 } finally { 391 put(ScriptEngine.FILENAME, null); 392 } 393 } 394 395 // scripts should see "context" and "engine" as variables 396 private void setContextVariables(final ScriptContext ctxt) { 397 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt); 398 // set "context" global variable via contextProperty - because this 399 // property is non-writable 400 contextProperty.setObjectValue(ctxtGlobal, ctxtGlobal, ctxt, false); 401 Object args = ScriptObjectMirror.unwrap(ctxt.getAttribute("arguments"), ctxtGlobal); 402 if (args == null || args == UNDEFINED) { 403 args = ScriptRuntime.EMPTY_ARRAY; 404 } 405 // if no arguments passed, expose it 406 if (! (args instanceof ScriptObject)) { 407 args = ((GlobalObject)ctxtGlobal).wrapAsObject(args); 408 ctxtGlobal.set("arguments", args, false); 409 } 410 } 411 412 private Object invokeImpl(final Object selfObject, final String name, final Object... args) throws ScriptException, NoSuchMethodException { 413 name.getClass(); // null check 414 415 ScriptObjectMirror selfMirror = null; 416 if (selfObject instanceof ScriptObjectMirror) { 417 selfMirror = (ScriptObjectMirror)selfObject; 418 if (! selfMirror.getHomeGlobal().isOfContext(nashornContext)) { 419 throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); 420 } 421 } else if (selfObject instanceof ScriptObject) { 422 // invokeMethod called from script code - in which case we may get 'naked' ScriptObject 423 // Wrap it with oldGlobal to make a ScriptObjectMirror for the same. 424 final ScriptObject oldGlobal = Context.getGlobal(); 425 if (oldGlobal == null) { 426 throw new IllegalArgumentException(getMessage("no.current.nashorn.global")); 427 } 428 429 if (! oldGlobal.isOfContext(nashornContext)) { 430 throw new IllegalArgumentException(getMessage("script.object.from.another.engine")); 431 } 432 433 selfMirror = (ScriptObjectMirror)ScriptObjectMirror.wrap(selfObject, oldGlobal); 434 } else if (selfObject == null) { 435 // selfObject is null => global function call 436 final ScriptObject ctxtGlobal = getNashornGlobalFrom(context); 437 selfMirror = (ScriptObjectMirror)ScriptObjectMirror.wrap(ctxtGlobal, ctxtGlobal); 438 } 439 440 if (selfMirror != null) { 441 try { 442 return ScriptObjectMirror.translateUndefined(selfMirror.call(name, args)); 443 } catch (final Exception e) { 444 final Throwable cause = e.getCause(); 445 if (cause instanceof NoSuchMethodException) { 446 throw (NoSuchMethodException)cause; 447 } 448 throwAsScriptException(e); 449 throw new AssertionError("should not reach here"); 450 } 451 } 452 453 // Non-script object passed as selfObject 454 throw new IllegalArgumentException(getMessage("interface.on.non.script.object")); 455 } 456 457 private Object evalImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException { 458 return evalImpl(compileImpl(buf, ctxt), ctxt); 459 } 460 461 private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt) throws ScriptException { 462 if (script == null) { 463 return null; 464 } 465 final ScriptObject oldGlobal = Context.getGlobal(); 466 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt); 467 final boolean globalChanged = (oldGlobal != ctxtGlobal); 468 try { 469 if (globalChanged) { 470 Context.setGlobal(ctxtGlobal); 471 } 472 473 setContextVariables(ctxt); 474 return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(ScriptRuntime.apply(script, ctxtGlobal), ctxtGlobal)); 475 } catch (final Exception e) { 476 throwAsScriptException(e); 477 throw new AssertionError("should not reach here"); 478 } finally { 479 if (globalChanged) { 480 Context.setGlobal(oldGlobal); 481 } 482 } 483 } 484 485 private static void throwAsScriptException(final Exception e) throws ScriptException { 486 if (e instanceof ScriptException) { 487 throw (ScriptException)e; 488 } else if (e instanceof NashornException) { 489 final NashornException ne = (NashornException)e; 490 final ScriptException se = new ScriptException( 491 ne.getMessage(), ne.getFileName(), 492 ne.getLineNumber(), ne.getColumnNumber()); 493 se.initCause(e); 494 throw se; 495 } else if (e instanceof RuntimeException) { 496 throw (RuntimeException)e; 497 } else { 498 // wrap any other exception as ScriptException 499 throw new ScriptException(e); 500 } 501 } 502 503 private CompiledScript asCompiledScript(final ScriptFunction script) { 504 return new CompiledScript() { 505 @Override 506 public Object eval(final ScriptContext ctxt) throws ScriptException { 507 return evalImpl(script, ctxt); 508 } 509 @Override 510 public ScriptEngine getEngine() { 511 return NashornScriptEngine.this; 512 } 513 }; 514 } 515 516 private ScriptFunction compileImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException { 517 final Object val = ctxt.getAttribute(ScriptEngine.FILENAME); 518 final String fileName = (val != null) ? val.toString() : "<eval>"; 519 return compileImpl(new Source(fileName, buf), ctxt); 520 } 521 522 private ScriptFunction compileImpl(final Source source, final ScriptContext ctxt) throws ScriptException { 523 final ScriptObject oldGlobal = Context.getGlobal(); 524 final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt); 525 final boolean globalChanged = (oldGlobal != ctxtGlobal); 526 try { 527 if (globalChanged) { 528 Context.setGlobal(ctxtGlobal); 529 } 530 531 return nashornContext.compileScript(source, ctxtGlobal); 532 } catch (final Exception e) { 533 throwAsScriptException(e); 534 throw new AssertionError("should not reach here"); 535 } finally { 536 if (globalChanged) { 537 Context.setGlobal(oldGlobal); 538 } 539 } 540 } 541 542 private static boolean isInterfaceImplemented(final Class<?> iface, final ScriptObject sobj) { 543 for (final Method method : iface.getMethods()) { 544 // ignore methods of java.lang.Object class 545 if (method.getDeclaringClass() == Object.class) { 546 continue; 547 } 548 549 Object obj = sobj.get(method.getName()); 550 if (! (obj instanceof ScriptFunction)) { 551 return false; 552 } 553 } 554 return true; 555 } 556 }