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