Sunday, March 30, 2008

Tips for JRuby engine: invokeMethod usage

This is the fourth post of "Tips for JRuby engine" series I've written in this blog and shows how to use javax.script.Invocable#invokeMethod() method defined in JSR 223 APIs. The idea to invoke Ruby's methods from Java is identical to the one described in the former post. It would greatly help you to understand about arguments and retuen values of invokeMethod() method.

As I explained in the former post, invokeMeothd() is used when Ruby's methods to be invoked from Java are resides in classes or modules. Except the first argument, invokeMethod() usage is the same as the one of invokeFunction(). The first argument is an instance of a Ruby class that has a method to be invoked. Consequently, Ruby code must return the instance so that Java can identify whose method it is and then invoke it. Ruby's instance can be get by a returned value when the Ruby script is evaled; therefore, before using invokeMethod(), a programmer must have the line, "Object object = engine.eval(script);", in his or her code to get the instance. The simplest code might be look like this:
String script =
"module SomeModule\n" +
"def say()" +
"puts \"Hi, there!\"" +
"end\n" +
"end\n" +
"class SomeClass\n" +
"include SomeModule;" +
"end\n" +
"SomeClass.new";
Object object = engine.eval(script);
Invocable invocable = (Invocable) engine;
invocable.invokeMethod(object, "say");
script =
"class AnotherClass\n" +
"def say_it_again()" +
"puts \"OK. I said, \'Hi, there.\'\"" +
"end\n" +
"end\n" +
"AnotherClass.new";
object = engine.eval(script);
invocable.invokeMethod(object, "say_it_again");
The first script has a method defined in a module and returns an instance of a class that includes the module as well as the method. The return value of JSR 223 APIs' eval() method should be the Ruby created instance and be the first argument of invokeMethod(). The second script is simpler than the first one - a class has a method and returns an instance. After evaluatig the script, Ruby defined method gets run over invokeMethod() method by using the returned instance.

If a programmer want to get more than one instance from single eval() method of a script, how can he or she get those instacnes from Ruby? We have two possiple ways of doing this; one is to get java.util.List typed object as a return value, and another is to use gobal variables. Ruby allows us to return more than one value at a time, so we can get multiple instances Ruby created by elements of List typed object. The folloing snippet shows how to get multiple instaces and invoke Ruby's method over Java's invokeMethod():
String script = 
"class Flowers\n" +
"@@hash = {\'red\' => \'ruby\', \'white\' => \'pearl\'}\n" +
"def initialize(color, names)" +
"@color = color;" +
"@names = names;" +
"end\n" +
"def comment\n" +
"puts \"#{@names.join(\', \')}. Beautiful like a #{@@hash[@color]}!\"" +
"end\n" +
"def others(index)" +
"print \"If I omit #{@names[index]}, \";" +
"@names.delete_at(index);" +
"print \"others are #{@names.join(\', \')}.\n\"" +
"end\n" +
"end\n" +
"red = Flowers.new(\"red\", [\"cameliia\", \"hibiscus\", \"rose\", \"canna\"])\n" +
"white = Flowers.new(\"white\", [\"gardenia\", \"lily\", \"daisy\"])\n" +
"return red, white";
Object objects = engine.eval(script);
Invocable invocable = (Invocable) engine;
if (objects instanceof List) {
for (Object object : (List)objects) {
invocable.invokeMethod(object, "comment");
invocable.invokeMethod(object, "others", 1);
}
}

The Ruby script above defines two methods, comment and others, in the Flowers class. Just below the class definition, it created two instances whose names are red and white, then returns both instances at a time. In Java code, two instaces must be returned by packing in java.lang.Object typed single instance; thus, we should cast it into java.util.List type and take each instance out from it. Each instance is set to the first argument of invokeMethod().

Another way of getting Ruby created multiple instaces is to use global variables. Instead of returning values, instances are given over to Java by delaring them global variables. The snippet below shows how to do this:
String script = 
"class Flowers\n" +
"@@hash = {\'red\' => \'ruby\', \'white\' => \'pearl\'}\n" +
"def initialize(color, names)" +
"@color = color;" +
"@names = names;" +
"end\n" +
"def comment\n" +
"puts \"#{@names.join(\', \')}. Beautiful like a #{@@hash[@color]}!\"" +
"end\n" +
"def others(index)" +
"print \"If I omit #{@names[index]}, \";" +
"@names.delete_at(index);" +
"print \"others are #{@names.join(\', \')}.\n\"" +
"end\n" +
"end\n" +
"$red = Flowers.new(\"red\", [\"cameliia\", \"hibiscus\", \"rose\", \"canna\"])\n" +
"$white = Flowers.new(\"white\", [\"gardenia\", \"lily\", \"daisy\"])";
engine.eval(script);
Object red = engine.get("red");
Object white = engine.get("white");
Invocable invocable = (Invocable) engine;
invocable.invokeMethod(red, "comment");
invocable.invokeMethod(white, "comment");
invocable.invokeMethod(red, "others", 1);
invocable.invokeMethod(white, "others", 2);

Ruby's class difinition is exactly identical to the one that returns multiple instances. The differences are the last part that global variables, red and white, are used to assign instacnes. In Java code, two instances are get through JRuby engine's context.

Please read my former post to know how to get return value from method invocations or how to use global variables in methods.

Following code is an enitre one to perfome snippets illustrated here:
package canna;

import java.util.List;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class InvokingMethodsExample {
private InvokingMethodsExample()
throws ScriptException, NoSuchMethodException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");
invokeSimpleMethod(engine);
invokeMethodWithMultipleInstances(engine);
invokeMethodWithGlobalVariables(engine);
}

private void invokeSimpleMethod(ScriptEngine engine)
throws ScriptException, NoSuchMethodException {
String script =
"module SomeModule\n" +
"def say()" +
"puts \"Hi, there!\"" +
"end\n" +
"end\n" +
"class SomeClass\n" +
"include SomeModule;" +
"end\n" +
"SomeClass.new";
Object object = engine.eval(script);
Invocable invocable = (Invocable) engine;
invocable.invokeMethod(object, "say");
script =
"class AnotherClass\n" +
"def say_it_again()" +
"puts \"OK. I said, \'Hi, there.\'\"" +
"end\n" +
"end\n" +
"AnotherClass.new";
object = engine.eval(script);
invocable.invokeMethod(object, "say_it_again");
}

private void invokeMethodWithMultipleInstances(ScriptEngine engine)
throws ScriptException, NoSuchMethodException {
String script =
"class Flowers\n" +
"@@hash = {\'red\' => \'ruby\', \'white\' => \'pearl\'}\n" +
"def initialize(color, names)" +
"@color = color;" +
"@names = names;" +
"end\n" +
"def comment\n" +
"puts \"#{@names.join(\', \')}. Beautiful like a #{@@hash[@color]}!\"" +
"end\n" +
"def others(index)" +
"print \"If I omit #{@names[index]}, \";" +
"@names.delete_at(index);" +
"print \"others are #{@names.join(\', \')}.\n\"" +
"end\n" +
"end\n" +
"red = Flowers.new(\"red\", [\"cameliia\", \"hibiscus\", \"rose\", \"canna\"])\n" +
"white = Flowers.new(\"white\", [\"gardenia\", \"lily\", \"daisy\"])\n" +
"return red, white";
Object objects = engine.eval(script);
Invocable invocable = (Invocable) engine;
if (objects instanceof List) {
for (Object object : (List)objects) {
invocable.invokeMethod(object, "comment");
invocable.invokeMethod(object, "others", 1);
}
}
}

private void invokeMethodWithGlobalVariables(ScriptEngine engine)
throws ScriptException, NoSuchMethodException {
String script =
"class Flowers\n" +
"@@hash = {\'red\' => \'ruby\', \'white\' => \'pearl\'}\n" +
"def initialize(color, names)" +
"@color = color;" +
"@names = names;" +
"end\n" +
"def comment\n" +
"puts \"#{@names.join(\', \')}. Beautiful like a #{@@hash[@color]}!\"" +
"end\n" +
"def others(index)" +
"print \"If I omit #{@names[index]}, \";" +
"@names.delete_at(index);" +
"print \"others are #{@names.join(\', \')}.\n\"" +
"end\n" +
"end\n" +
"$red = Flowers.new(\"red\", [\"cameliia\", \"hibiscus\", \"rose\", \"canna\"])\n" +
"$white = Flowers.new(\"white\", [\"gardenia\", \"lily\", \"daisy\"])";
engine.eval(script);
Object red = engine.get("red");
Object white = engine.get("white");
Invocable invocable = (Invocable) engine;
invocable.invokeMethod(red, "comment");
invocable.invokeMethod(white, "comment");
invocable.invokeMethod(red, "others", 1);
invocable.invokeMethod(white, "others", 2);
}

public static void main(String[] args) throws ScriptException, NoSuchMethodException {
new InvokingMethodsExample();
}
}

If this code gets run successfully, it produces outputs below:
Hi, there!
OK. I said, 'Hi, there.'
cameliia, hibiscus, rose, canna. Beautiful like a ruby!
If I omit hibiscus, others are cameliia, rose, canna.
gardenia, lily, daisy. Beautiful like a pearl!
If I omit lily, others are gardenia, daisy.
cameliia, hibiscus, rose, canna. Beautiful like a ruby!
gardenia, lily, daisy. Beautiful like a pearl!
If I omit hibiscus, others are cameliia, rose, canna.
If I omit daisy, others are gardenia, lily.

No comments: