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 }