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