This is the fifth post of "Tips for JRuby engine" series I've written in this blog and shows how to use javax.script.Invocable#getInterface() method defined in JSR 223 APIs. This method is used when Ruby scripts provide implementations of Java defined interfaces. In other words, JSR 223 API allows us to write interfaces only in Java and implement them by Ruby. A programmer can discover interface-implemented, Ruby-created instances by using getInterface() method and invoke methods defined in the interfaces.
JSR 223 API defines two types of getInterface() method - <T> T getInterface(Class<T> clasz), and <T> T getInterface(Object thiz, Class<T> clasz). The difference of these two methods is similar to the one between invokeFunction() and invokeMethod(). While the former method is applied to the top-level methods, the latter is done to the methods defined in classes or modules. Likewise, <T> T getInterface(Class<T> clasz) is used when interface-defined methods are implemented by Ruby's top-level methods, and <T> T getInterface(Object thiz, Class<T> clasz) is for methods that Ruby scripts implemented in classes or modules.
If programmers want to implement Java interface by Ruby, they need at least two statements in Ruby script - first,
require 'java'
: second,
include_class
for top-level methods, or
import
for others to claim that the script is the implementation of some interfaces. Suppose the interface below has defined:
package canna;
import java.util.List;
public interface SimpleFile {
void create(String filename);
void write(List list);
void close();
}
When the programmers implement this interface by Ruby's top-level methods, Ruby script would be like this:
require 'java'
include_class 'canna.SimpleFile'
def create(name)
@name = name;
@tmpfile = File.new(name, "w");
@tmpfile.chmod(0600);
end
def write(message)
message.each { |m| @tmpfile.puts(m) }
end
def close()
@tmpfile.close;
puts "The file, #{@name}, has #{File.size(@name)} bytes."
end
Thie script has
require 'java'
and
include_class 'canna.SimpleFile'
statements in the beginning to load Java extension and to include Java interface, then defines create, write, and close methods that the interface, canna.SimpleFile, has. In case of top-level methods' implementation, script doesn't need to return any instance since JRuby engine gets a receiver object, "self," from JRuby's runtime internally. Utilizing Ruby's implementation, Java code would be:
engine.eval(script);
Invocable invocable = (Invocable) engine;
SimpleFile simpleFile = invocable.getInterface(SimpleFile.class);
simpleFile.create("simplefile.txt");
List list = new ArrayList();
list.add("A bird in the hand is worth two in the bush.");
list.add("Birds of a feather flock together.");
list.add("Every bird loves to hear himself sing.");
simpleFile.write(list);
simpleFile.close();
Once, we run this code, we'll find the file whose name is simlefile.txt in file system and see three lines in it. As we expect, simplfile.txt has the mode 0600:
-rw------- 1 yoko staff 119 4 2 14:06 simplefile.txt
How should the programmers write a code if they want to implemment Java interfaces by defiening Ruby classes? The Ruby script must have
require 'java'
statement prior to a class definition, and
import 'canna.SimpleFile'
statement in a class definition. Besides, the script must return more than one instance. Thus, it would look like:
require 'java'
class SimpleFileImple
import 'canna.SimpleFile'
def initialize(name)
@name = name;
@tmpfile = File.new(name, "w");
@tmpfile.chmod(0600)
end
def write(message)
message.each { |m| @tmpfile.puts(m) }
end
def close()
@tmpfile.close;
puts "The file, #{@name}, has #{File.size(@name)} bytes."
end
end
SimpleFileImple.new($name)
SimpleFileImple class implements Java-defined canna.SimpleFile interface, and has a constructor, and write and close methods definition, but not create method. Ruby doesn't complain that not all methods of the interface imported are covered. Then, Java code would be written below:
engine.put("name", "simplefile2.txt");
Object object = engine.eval(script);
Invocable invocable = (Invocable) engine;
SimpleFile simpleFile = invocable.getInterface(object, SimpleFile.class);
List list = new ArrayList();
list.add("When it is a question of money, everybody is of the same religion.");
list.add("Money is the wise man's religion.");
simpleFile.write(list);
simpleFile.close();
This code gets java.lang.Object typed instance when the Ruby script is evaluated. This instance must be created by Ruby and is able to be casted into canna.SimpleFile type, or getInterface() method doesn't work correctly. The argument of the constructor, which is a file name, is passed by using a global variable
name
, so
simplefile2.txt
will appear after the code gets run.
At last, I'll demonstrate more complicated but more real example. In a real application, people often implement more than one interface in a single class, and instantiate mutiple objects based on the single definition. To see how to do this, let's create very simple two interfaces,
canna.Remarkable
and
canna.Removable
, shown below:
package canna;
public interface Remarkable {
void remark();
}
package canna;
public interface Removable {
void remove(int i);
}
Then, think about what the Ruby script would be. We need to have
require 'java'
and two
import
statements in it, plus, methods' implementations; moreover, we are going to create and return two instances to give over to Java. Considering these requirements, we would write following Ruby script to wrap them up.
require 'java'
class Flowers
import 'canna.Remarkable'
import 'canna.Removable'
@@hash = {'red' => 'ruby', 'white' => 'pearl'}
def initialize(color, names)
@color = color;
@names = names;
end
def remark
puts "#{@names.join(', ')}. Beautiful like a #{@@hash[@color]}!"
end
def remove(index)
print "If I remove #{@names[index]}, ";
@names.delete_at(index);
print "others will be #{@names.join(', ')}."
end
end
red = Flowers.new("red", ["cameliia", "hibiscus", "rose", "canna"])
white = Flowers.new("white", ["gardenia", "lily", "magnolia"])
return red, white
When this script is evaluated, we'll get a java.util.List type object that has multiple return values in its elements. Therefore, in Java code, we would take each instance out from the List type object, and invoke methods defined in the interfaces, like this:
Object objects = engine.eval(script);
Invocable invocable = (Invocable) engine;
if (objects instanceof List) {
for (Object object : (List)objects) {
Object flower = invocable.getInterface(object, Remarkable.class);
((Remarkable)flower).remark();
flower = invocable.getInterface(object, Removable.class);
((Removable)flower).remove(1);
}
}
This is an interesting feature of JSR 223 APIs; however, old versions of JRuby had a problem to execute getInterface() method correctly. Make sure your JRuby is 1.1RC2 or later.
Here are entire codes to perform all snippets I mentioned here:
//Remarkable.java
package canna;
public interface Remarkable {
void remark();
}
//Removable.java
package canna;
public interface Removable {
void remove(int i);
}
//GetInterfacesExample.java
package canna;
import java.util.ArrayList;
import java.util.List;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class GetInterfacesExample {
private GetInterfacesExample() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");
getInterfaceByTopLevel(engine);
getInterfaceByClass(engine);
getInterfaceMultipleStuffs(engine);
}
private void getInterfaceByTopLevel(ScriptEngine engine) throws ScriptException {
String script =
"require \'java\'\n" +
"include_class \'canna.SimpleFile\'\n" +
"def create(name)" +
"@name = name;" +
"@tmpfile = File.new(name, \"w\");" +
"@tmpfile.chmod(0600);" +
"end\n" +
"def write(message)" +
"message.each { |m| @tmpfile.puts(m) }" +
"end\n" +
"def close()" +
"@tmpfile.close;" +
"puts \"The file, #{@name}, has #{File.size(@name)} bytes.\"" +
"end";
engine.eval(script);
Invocable invocable = (Invocable) engine;
SimpleFile simpleFile = invocable.getInterface(SimpleFile.class);
simpleFile.create("simplefile.txt");
List list = new ArrayList();
list.add("A bird in the hand is worth two in the bush.");
list.add("Birds of a feather flock together.");
list.add("Every bird loves to hear himself sing.");
simpleFile.write(list);
simpleFile.close();
}
private void getInterfaceByClass(ScriptEngine engine) throws ScriptException {
String script =
"class SimpleFileImple\n" +
"import \'canna.SimpleFile\'\n" +
"def initialize(name)" +
"@name = name;" +
"@tmpfile = File.new(name, \"w\");" +
"@tmpfile.chmod(0600)" +
"end\n" +
"def write(message)" +
"message.each { |m| @tmpfile.puts(m) }" +
"end\n" +
"def close()" +
"@tmpfile.close;" +
"puts \"The file, #{@name}, has #{File.size(@name)} bytes.\"" +
"end\n" +
"end\n" +
"SimpleFileImple.new($name)";
engine.put("name", "simplefile2.txt");
Object object = engine.eval(script);
Invocable invocable = (Invocable) engine;
SimpleFile simpleFile = invocable.getInterface(object, SimpleFile.class);
List list = new ArrayList();
list.add("When it is a question of money, everybody is of the same religion.");
list.add("Money is the wise man's religion.");
simpleFile.write(list);
simpleFile.close();
}
private void getInterfaceMultipleStuffs(ScriptEngine engine) throws ScriptException {
String script =
"class Flowers\n" +
"import \'canna.Remarkable\'\n" +
"import \'canna.Removable\'\n" +
"@@hash = {\'red\' => \'ruby\', \'white\' => \'pearl\'}\n" +
"def initialize(color, names)" +
"@color = color;" +
"@names = names;" +
"end\n" +
"def remark\n" +
"puts \"#{@names.join(\', \')}. Beautiful like a #{@@hash[@color]}!\"" +
"end\n" +
"def remove(index)" +
"print \"If I remove #{@names[index]}, \";" +
"@names.delete_at(index);" +
"print \"others will be #{@names.join(\', \')}.\n\"" +
"end\n" +
"end\n" +
"red = Flowers.new(\"red\", [\"cameliia\", \"hibiscus\", \"rose\", \"canna\"])\n" +
"white = Flowers.new(\"white\", [\"gardenia\", \"lily\", \"magnolia\"])\n" +
"return red, white";
Object objects = engine.eval(script);
Invocable invocable = (Invocable) engine;
if (objects instanceof List) {
for (Object object : (List)objects) {
Object flower = invocable.getInterface(object, Remarkable.class);
((Remarkable)flower).remark();
flower = invocable.getInterface(object, Removable.class);
((Removable)flower).remove(1);
}
}
}
public static void main(String[] args)
throws ScriptException, NoSuchMethodException {
new GetInterfacesExample();
}
}
In above code, the second and third script don't have
require 'java'
statement because the first script has loaded java extension onto JRuby runtime and is valid until the program exits.
If these code gets run successfully, they produce outputs below:
he file, simplefile.txt, has 119 bytes.
The file, simplefile2.txt, has 101 bytes.
cameliia, hibiscus, rose, canna. Beautiful like a ruby!
If I remove hibiscus, others will be cameliia, rose, canna.
gardenia, lily, magnolia. Beautiful like a pearl!
If I remove lily, others will be gardenia, magnolia.
In addition, two files will be created on file system and have the mode 0600:
-rw------- 1 yoko staff 119 4 2 14:06 simplefile.txt
-rw------- 1 yoko staff 101 4 2 14:06 simplefile2.txt