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