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", "-af", "-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 }