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 java.util.Objects;
  29 import java.util.function.Function;
  30 import javax.script.Invocable;
  31 import javax.script.ScriptContext;
  32 import javax.script.ScriptEngine;
  33 import javax.script.ScriptEngineManager;
  34 import javax.script.ScriptException;
  35 import javax.script.SimpleScriptContext;
  36 import org.testng.Assert;
  37 import static org.testng.Assert.assertEquals;
  38 import static org.testng.Assert.fail;
  39 import org.testng.annotations.Test;
  40 
  41 /**
  42  * Tests for javax.script.Invocable implementation of nashorn.
  43  */
  44 public class InvocableTest {
  45 
  46     private void log(String msg) {
  47         org.testng.Reporter.log(msg, true);
  48     }
  49 
  50     @Test
  51     public void invokeMethodTest() {
  52         final ScriptEngineManager m = new ScriptEngineManager();
  53         final ScriptEngine e = m.getEngineByName("nashorn");
  54 
  55         try {
  56             e.eval("var Example = function() { this.hello = function() { return 'Hello World!'; };}; myExample = new Example();");
  57             final Object obj = e.get("myExample");
  58             final Object res = ((Invocable) e).invokeMethod(obj, "hello");
  59             assertEquals(res, "Hello World!");
  60         } catch (final Exception exp) {
  61             exp.printStackTrace();
  62             fail(exp.getMessage());
  63         }
  64     }
  65 
  66     @Test
  67     /**
  68      * Check that we can call invokeMethod on an object that we got by
  69      * evaluating script with different Context set.
  70      */
  71     public void invokeMethodDifferentContextTest() {
  72         ScriptEngineManager m = new ScriptEngineManager();
  73         ScriptEngine e = m.getEngineByName("nashorn");
  74 
  75         try {
  76             // define an object with method on it
  77             Object obj = e.eval("({ hello: function() { return 'Hello World!'; } })");
  78 
  79             final ScriptContext ctxt = new SimpleScriptContext();
  80             ctxt.setBindings(e.createBindings(), ScriptContext.ENGINE_SCOPE);
  81             e.setContext(ctxt);
  82 
  83             // invoke 'func' on obj - but with current script context changed
  84             final Object res = ((Invocable) e).invokeMethod(obj, "hello");
  85             assertEquals(res, "Hello World!");
  86         } catch (final Exception exp) {
  87             exp.printStackTrace();
  88             fail(exp.getMessage());
  89         }
  90     }
  91 
  92     @Test
  93     /**
  94      * Check that invokeMethod throws NPE on null method name.
  95      */
  96     public void invokeMethodNullNameTest() {
  97         final ScriptEngineManager m = new ScriptEngineManager();
  98         final ScriptEngine e = m.getEngineByName("nashorn");
  99 
 100         try {
 101             final Object obj = e.eval("({})");
 102             final Object res = ((Invocable) e).invokeMethod(obj, null);
 103             fail("should have thrown NPE");
 104         } catch (final Exception exp) {
 105             if (!(exp instanceof NullPointerException)) {
 106                 exp.printStackTrace();
 107                 fail(exp.getMessage());
 108             }
 109         }
 110     }
 111 
 112     @Test
 113     /**
 114      * Check that invokeMethod throws NoSuchMethodException on missing method.
 115      */
 116     public void invokeMethodMissingTest() {
 117         final ScriptEngineManager m = new ScriptEngineManager();
 118         final ScriptEngine e = m.getEngineByName("nashorn");
 119 
 120         try {
 121             final Object obj = e.eval("({})");
 122             final Object res = ((Invocable) e).invokeMethod(obj, "nonExistentMethod");
 123             fail("should have thrown NoSuchMethodException");
 124         } catch (final Exception exp) {
 125             if (!(exp instanceof NoSuchMethodException)) {
 126                 exp.printStackTrace();
 127                 fail(exp.getMessage());
 128             }
 129         }
 130     }
 131 
 132     @Test
 133     /**
 134      * Check that calling method on non-script object 'thiz' results in
 135      * IllegalArgumentException.
 136      */
 137     public void invokeMethodNonScriptObjectThizTest() {
 138         final ScriptEngineManager m = new ScriptEngineManager();
 139         final ScriptEngine e = m.getEngineByName("nashorn");
 140 
 141         try {
 142             ((Invocable) e).invokeMethod(new Object(), "toString");
 143             fail("should have thrown IllegalArgumentException");
 144         } catch (final Exception exp) {
 145             if (!(exp instanceof IllegalArgumentException)) {
 146                 exp.printStackTrace();
 147                 fail(exp.getMessage());
 148             }
 149         }
 150     }
 151 
 152     @Test
 153     /**
 154      * Check that calling method on null 'thiz' results in
 155      * IllegalArgumentException.
 156      */
 157     public void invokeMethodNullThizTest() {
 158         final ScriptEngineManager m = new ScriptEngineManager();
 159         final ScriptEngine e = m.getEngineByName("nashorn");
 160 
 161         try {
 162             ((Invocable) e).invokeMethod(null, "toString");
 163             fail("should have thrown IllegalArgumentException");
 164         } catch (final Exception exp) {
 165             if (!(exp instanceof IllegalArgumentException)) {
 166                 exp.printStackTrace();
 167                 fail(exp.getMessage());
 168             }
 169         }
 170     }
 171 
 172     @Test
 173     /**
 174      * Check that calling method on mirror created by another engine results in
 175      * IllegalArgumentException.
 176      */
 177     public void invokeMethodMixEnginesTest() {
 178         final ScriptEngineManager m = new ScriptEngineManager();
 179         final ScriptEngine engine1 = m.getEngineByName("nashorn");
 180         final ScriptEngine engine2 = m.getEngineByName("nashorn");
 181 
 182         try {
 183             Object obj = engine1.eval("({ run: function() {} })");
 184             // pass object from engine1 to engine2 as 'thiz' for invokeMethod
 185             ((Invocable) engine2).invokeMethod(obj, "run");
 186             fail("should have thrown IllegalArgumentException");
 187         } catch (final Exception exp) {
 188             if (!(exp instanceof IllegalArgumentException)) {
 189                 exp.printStackTrace();
 190                 fail(exp.getMessage());
 191             }
 192         }
 193     }
 194 
 195     @Test
 196     public void getInterfaceTest() {
 197         final ScriptEngineManager m = new ScriptEngineManager();
 198         final ScriptEngine e = m.getEngineByName("nashorn");
 199         final Invocable inv = (Invocable) e;
 200 
 201         // try to get interface from global functions
 202         try {
 203             e.eval("function run() { print('run'); };");
 204             final Runnable runnable = inv.getInterface(Runnable.class);
 205             runnable.run();
 206         } catch (final Exception exp) {
 207             exp.printStackTrace();
 208             fail(exp.getMessage());
 209         }
 210 
 211         // try interface on specific script object
 212         try {
 213             e.eval("var obj = { run: function() { print('run from obj'); } };");
 214             Object obj = e.get("obj");
 215             final Runnable runnable = inv.getInterface(obj, Runnable.class);
 216             runnable.run();
 217         } catch (final Exception exp) {
 218             exp.printStackTrace();
 219             fail(exp.getMessage());
 220         }
 221     }
 222 
 223     public interface Foo {
 224 
 225         public void bar();
 226     }
 227 
 228     public interface Foo2 extends Foo {
 229 
 230         public void bar2();
 231     }
 232 
 233     @Test
 234     public void getInterfaceMissingTest() {
 235         final ScriptEngineManager manager = new ScriptEngineManager();
 236         final ScriptEngine engine = manager.getEngineByName("nashorn");
 237 
 238         // don't define any function.
 239         try {
 240             engine.eval("");
 241         } catch (final Exception exp) {
 242             exp.printStackTrace();
 243             fail(exp.getMessage());
 244         }
 245 
 246         Runnable runnable = ((Invocable) engine).getInterface(Runnable.class);
 247         if (runnable != null) {
 248             fail("runnable is not null!");
 249         }
 250 
 251         // now define "run"
 252         try {
 253             engine.eval("function run() { print('this is run function'); }");
 254         } catch (final Exception exp) {
 255             exp.printStackTrace();
 256             fail(exp.getMessage());
 257         }
 258         runnable = ((Invocable) engine).getInterface(Runnable.class);
 259         // should not return null now!
 260         runnable.run();
 261 
 262         // define only one method of "Foo2"
 263         try {
 264             engine.eval("function bar() { print('bar function'); }");
 265         } catch (final Exception exp) {
 266             exp.printStackTrace();
 267             fail(exp.getMessage());
 268         }
 269 
 270         Foo2 foo2 = ((Invocable) engine).getInterface(Foo2.class);
 271         if (foo2 != null) {
 272             throw new RuntimeException("foo2 is not null!");
 273         }
 274 
 275         // now define other method of "Foo2"
 276         try {
 277             engine.eval("function bar2() { print('bar2 function'); }");
 278         } catch (final Exception exp) {
 279             exp.printStackTrace();
 280             fail(exp.getMessage());
 281         }
 282         foo2 = ((Invocable) engine).getInterface(Foo2.class);
 283         foo2.bar();
 284         foo2.bar2();
 285     }
 286 
 287     @Test
 288     /**
 289      * Try passing non-interface Class object for interface implementation.
 290      */
 291     public void getNonInterfaceGetInterfaceTest() {
 292         final ScriptEngineManager manager = new ScriptEngineManager();
 293         final ScriptEngine engine = manager.getEngineByName("nashorn");
 294         try {
 295             log(Objects.toString(((Invocable) engine).getInterface(Object.class)));
 296             fail("Should have thrown IllegalArgumentException");
 297         } catch (final Exception exp) {
 298             if (!(exp instanceof IllegalArgumentException)) {
 299                 fail("IllegalArgumentException expected, got " + exp);
 300             }
 301         }
 302     }
 303 
 304     @Test
 305     /**
 306      * Check that we can get interface out of a script object even after
 307      * switching to use different ScriptContext.
 308      */
 309     public void getInterfaceDifferentContext() {
 310         ScriptEngineManager m = new ScriptEngineManager();
 311         ScriptEngine e = m.getEngineByName("nashorn");
 312         try {
 313             Object obj = e.eval("({ run: function() { } })");
 314 
 315             // change script context
 316             ScriptContext ctxt = new SimpleScriptContext();
 317             ctxt.setBindings(e.createBindings(), ScriptContext.ENGINE_SCOPE);
 318             e.setContext(ctxt);
 319 
 320             Runnable r = ((Invocable) e).getInterface(obj, Runnable.class);
 321             r.run();
 322         } catch (final Exception exp) {
 323             exp.printStackTrace();
 324             fail(exp.getMessage());
 325         }
 326     }
 327 
 328     @Test
 329     /**
 330      * Check that getInterface on non-script object 'thiz' results in
 331      * IllegalArgumentException.
 332      */
 333     public void getInterfaceNonScriptObjectThizTest() {
 334         final ScriptEngineManager m = new ScriptEngineManager();
 335         final ScriptEngine e = m.getEngineByName("nashorn");
 336 
 337         try {
 338             ((Invocable) e).getInterface(new Object(), Runnable.class);
 339             fail("should have thrown IllegalArgumentException");
 340         } catch (final Exception exp) {
 341             if (!(exp instanceof IllegalArgumentException)) {
 342                 exp.printStackTrace();
 343                 fail(exp.getMessage());
 344             }
 345         }
 346     }
 347 
 348     @Test
 349     /**
 350      * Check that getInterface on null 'thiz' results in
 351      * IllegalArgumentException.
 352      */
 353     public void getInterfaceNullThizTest() {
 354         final ScriptEngineManager m = new ScriptEngineManager();
 355         final ScriptEngine e = m.getEngineByName("nashorn");
 356 
 357         try {
 358             ((Invocable) e).getInterface(null, Runnable.class);
 359             fail("should have thrown IllegalArgumentException");
 360         } catch (final Exception exp) {
 361             if (!(exp instanceof IllegalArgumentException)) {
 362                 exp.printStackTrace();
 363                 fail(exp.getMessage());
 364             }
 365         }
 366     }
 367 
 368     @Test
 369     /**
 370      * Check that calling getInterface on mirror created by another engine
 371      * results in IllegalArgumentException.
 372      */
 373     public void getInterfaceMixEnginesTest() {
 374         final ScriptEngineManager m = new ScriptEngineManager();
 375         final ScriptEngine engine1 = m.getEngineByName("nashorn");
 376         final ScriptEngine engine2 = m.getEngineByName("nashorn");
 377 
 378         try {
 379             Object obj = engine1.eval("({ run: function() {} })");
 380             // pass object from engine1 to engine2 as 'thiz' for getInterface
 381             ((Invocable) engine2).getInterface(obj, Runnable.class);
 382             fail("should have thrown IllegalArgumentException");
 383         } catch (final Exception exp) {
 384             if (!(exp instanceof IllegalArgumentException)) {
 385                 exp.printStackTrace();
 386                 fail(exp.getMessage());
 387             }
 388         }
 389     }
 390 
 391     @Test
 392     /**
 393      * check that null function name results in NPE.
 394      */
 395     public void invokeFunctionNullNameTest() {
 396         final ScriptEngineManager m = new ScriptEngineManager();
 397         final ScriptEngine e = m.getEngineByName("nashorn");
 398 
 399         try {
 400             final Object res = ((Invocable) e).invokeFunction(null);
 401             fail("should have thrown NPE");
 402         } catch (final Exception exp) {
 403             if (!(exp instanceof NullPointerException)) {
 404                 exp.printStackTrace();
 405                 fail(exp.getMessage());
 406             }
 407         }
 408     }
 409 
 410     @Test
 411     /**
 412      * Check that attempt to call missing function results in
 413      * NoSuchMethodException.
 414      */
 415     public void invokeFunctionMissingTest() {
 416         final ScriptEngineManager m = new ScriptEngineManager();
 417         final ScriptEngine e = m.getEngineByName("nashorn");
 418 
 419         try {
 420             final Object res = ((Invocable) e).invokeFunction("NonExistentFunc");
 421             fail("should have thrown NoSuchMethodException");
 422         } catch (final Exception exp) {
 423             if (!(exp instanceof NoSuchMethodException)) {
 424                 exp.printStackTrace();
 425                 fail(exp.getMessage());
 426             }
 427         }
 428     }
 429 
 430     @Test
 431     /**
 432      * Check that invokeFunction calls functions only from current context's
 433      * Bindings.
 434      */
 435     public void invokeFunctionDifferentContextTest() {
 436         ScriptEngineManager m = new ScriptEngineManager();
 437         ScriptEngine e = m.getEngineByName("nashorn");
 438 
 439         try {
 440             // define an object with method on it
 441             Object obj = e.eval("function hello() { return 'Hello World!'; }");
 442             final ScriptContext ctxt = new SimpleScriptContext();
 443             ctxt.setBindings(e.createBindings(), ScriptContext.ENGINE_SCOPE);
 444             // change engine's current context
 445             e.setContext(ctxt);
 446 
 447             ((Invocable) e).invokeFunction("hello"); // no 'hello' in new context!
 448             fail("should have thrown NoSuchMethodException");
 449         } catch (final Exception exp) {
 450             if (!(exp instanceof NoSuchMethodException)) {
 451                 exp.printStackTrace();
 452                 fail(exp.getMessage());
 453             }
 454         }
 455     }
 456 
 457     @Test
 458     public void invokeFunctionExceptionTest() {
 459         final ScriptEngineManager m = new ScriptEngineManager();
 460         final ScriptEngine e = m.getEngineByName("nashorn");
 461         try {
 462             e.eval("function func() { throw new TypeError(); }");
 463         } catch (final Throwable t) {
 464             t.printStackTrace();
 465             fail(t.getMessage());
 466         }
 467 
 468         try {
 469             ((Invocable) e).invokeFunction("func");
 470             fail("should have thrown exception");
 471         } catch (final ScriptException se) {
 472             // ECMA TypeError property wrapped as a ScriptException
 473             log("got " + se + " as expected");
 474         } catch (final Throwable t) {
 475             t.printStackTrace();
 476             fail(t.getMessage());
 477         }
 478     }
 479 
 480     @Test
 481     public void invokeMethodExceptionTest() {
 482         final ScriptEngineManager m = new ScriptEngineManager();
 483         final ScriptEngine e = m.getEngineByName("nashorn");
 484         try {
 485             e.eval("var sobj = {}; sobj.foo = function func() { throw new TypeError(); }");
 486         } catch (final Throwable t) {
 487             t.printStackTrace();
 488             fail(t.getMessage());
 489         }
 490 
 491         try {
 492             final Object sobj = e.get("sobj");
 493             ((Invocable) e).invokeMethod(sobj, "foo");
 494             fail("should have thrown exception");
 495         } catch (final ScriptException se) {
 496             // ECMA TypeError property wrapped as a ScriptException
 497             log("got " + se + " as expected");
 498         } catch (final Throwable t) {
 499             t.printStackTrace();
 500             fail(t.getMessage());
 501         }
 502     }
 503 
 504     @Test
 505     /**
 506      * Tests whether invocation of a JavaScript method through a variable arity
 507      * Java method will pass the vararg array. Both non-vararg and vararg
 508      * JavaScript methods are tested.
 509      *
 510      * @throws ScriptException
 511      */
 512     public void variableArityInterfaceTest() throws ScriptException {
 513         final ScriptEngineManager m = new ScriptEngineManager();
 514         final ScriptEngine e = m.getEngineByName("nashorn");
 515         e.eval(
 516                 "function test1(i, strings) {"
 517                 + "    return 'i == ' + i + ', strings instanceof java.lang.String[] == ' + (strings instanceof Java.type('java.lang.String[]')) + ', strings == ' + java.util.Arrays.toString(strings)"
 518                 + "}"
 519                 + "function test2() {"
 520                 + "    return 'arguments[0] == ' + arguments[0] + ', arguments[1] instanceof java.lang.String[] == ' + (arguments[1] instanceof Java.type('java.lang.String[]')) + ', arguments[1] == ' + java.util.Arrays.toString(arguments[1])"
 521                 + "}");
 522         final VariableArityTestInterface itf = ((Invocable) e).getInterface(VariableArityTestInterface.class);
 523         Assert.assertEquals(itf.test1(42, "a", "b"), "i == 42, strings instanceof java.lang.String[] == true, strings == [a, b]");
 524         Assert.assertEquals(itf.test2(44, "c", "d", "e"), "arguments[0] == 44, arguments[1] instanceof java.lang.String[] == true, arguments[1] == [c, d, e]");
 525     }
 526 
 527     @Test
 528     @SuppressWarnings("unchecked")
 529     public void defaultMethodTest() throws ScriptException {
 530         final ScriptEngineManager m = new ScriptEngineManager();
 531         final ScriptEngine e = m.getEngineByName("nashorn");
 532         final Invocable inv = (Invocable) e;
 533 
 534         Object obj = e.eval("({ apply: function(arg) { return arg.toUpperCase(); }})");
 535         Function<String, String> func = inv.getInterface(obj, Function.class);
 536         assertEquals(func.apply("hello"), "HELLO");
 537     }
 538 }