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