This section shows how to define multiple classes in Ruby that are all implemented and backed by only one class in Java.
Depending on the Ruby class that is being instantiated the Java class is instantiated with different properties.
The following example builds a class in Java that has one method getCurrentTime()
that formats the current time using a java.util.DateFormat
using either the SHORT
, MEDIUM
, LONG
or FULL
style.
A parameter passed to the constructor decides what format to use.
In Ruby there should be one class name per style, i.e. there should be classes ShortTime
, MediumTime
, LongTime
and FullTime
and the method current_time
of each class returns the respective string.
First the Java class that backs the Ruby classes has to be implemented.
It extends RubyObject and gets the style constant passed to its constructor, i.e. DateFormat.SHORT
, DateFormat.MEDIUM
, …
public class TimeFormatter extends RubyObject { (1)
private final DateFormat timeFormat;
public TimeFormatter(Ruby runtime, RubyClass metaClass, int format) {
super(runtime, metaClass);
this.timeFormat = DateFormat.getTimeInstance(format, Locale.ENGLISH);
}
@JRubyMethod(name = "current_time")
public IRubyObject getCurrentTime() { (2)
String s = timeFormat.format(new Date());
return JavaEmbedUtils.javaToRuby(getRuntime(), s);
}
}
-
Every Java class that directly backs a Ruby class must implement IRubyObject
.
The class RubyObject
already implements all required infrastructure methods.
-
The argument list can be empty.
We don’t need a ThreadContext
.
The method can be non-static so that we don’t need a self
parameter in the argument list because this
is the instance that backs the Ruby object.
Now comes the final step.
For each time style a RubyClass
is created with an appropriate name.
When creating a new Ruby class using the JRuby API a name, a super class and an ObjectAllocator
that creates backing instances must be given.
The ObjectAllocator
is the link that creates new TimeFormatter
instances parameterized with the correct style constant.
RubyModule module = rubyRuntime.getOrCreateModule("TestModule");
for (String format: Arrays.asList("Short", "Medium", "Long", "Full")) {
final int formatConstant = DateFormat.class.getField(format.toUpperCase()).getInt(null); (1)
RubyClass rubyClass = module.defineClassUnder(
format + "Time", (2)
rubyRuntime.getObject(), (3)
new ObjectAllocator() { (4)
@Override
public IRubyObject allocate(Ruby ruby, RubyClass rubyClass) { (5)
return new TimeFormatter(ruby, rubyClass, formatConstant);
}
});
rubyClass.defineAnnotatedMethods(TimeFormatter.class);
}
rubyRuntime.evalScriptlet("puts TestModule::ShortTime.new.current_time"); (6)
rubyRuntime.evalScriptlet("puts TestModule::MediumTime.new.current_time");
rubyRuntime.evalScriptlet("puts TestModule::LongTime.new.current_time");
rubyRuntime.evalScriptlet("puts TestModule::FullTime.new.current_time");
-
Get the style constant from java.util.DateFormat
using reflection, i.e. DateFormat.SHORT
, DateFormat.MEDIUM
, etc.
-
The name of the new classes should be ShortTime
, MediumTime
, LongTime
and FullTime
.
-
The Ruby super class for each new class should be Rubys Object
class.
-
Each ObjectAllocator
instance for each Ruby class creates new instances of the Java TimeFormatter
class passing the respective style constant.
-
The allocate
method is calledby JRuby every time a new instance of the Ruby class is created.
That is JRuby delegates instantiation of the Java class that backs the Ruby class to this method.
-
Finally instantiate each class in Ruby and invoke the same method for every class showing a different behavior every time.
The output of the code above is:
7:22 PM
7:22:33 PM
7:22:33 PM CET
7:22:33 PM CET
Another advantage of using this approach versus implicitely mapping Java class to Ruby classes is that you have a very fine grained control of the methods that are visible in the Ruby world.
Otherwise even the methods defined on java.lang.Object
are available to Ruby code.