Last time, I demonstrated how to re-open Java objects from JRuby and I used a completely worthless example. This time, the example is only mostly worthless. By that I mean, we’re going to add some Ruby-like aesthetics to a very useful Java library called XOM. The perceived value of this code is directly proportional to how much you believe you should be able to use each, any?, all?, include?, or grep on all collection-like objects from JRuby code.
XOM is a well designed and very opinionated Java library. It is also my preferred library for general purpose XML wrangling. However, because of the principles under which XOM was developed (not implementing collections API, I’m looking at you), certain Ruby conveniences don’t get applied to XOM objects in JRuby. In particular, XOM’s collections don’t implement the Java collections framework. Because of this, XOM collections don’t get decorated with Ruby-like iterators and such. We’re going to change that.
Before we get started, make sure you can reach XOM from JRuby:
1) Download XOM, of course.
2) Set or export your CLASSPATH so that it includes the XOM jar.
3) Fire up jirb.
The classes we’re looking at are Nodes and Elements. I would like for both of these classes to behave more like Ruby array objects. For the Nodes class, which is a modifiable collection, I want [] to be an alias for get, I want << to be an alias for append, and I want to mix in the Ruby Enumerable module. For Elements, which is a read-only collection, I want the [] alias and Enumerable.
So first things first:
require 'java' XomNodes = Java::nu.xom.Nodes XomElement = Java::nu.xom.Element XomElements = Java::nu.xom.Elements
This should look familiar, but you’ll notice that we used the Java module to reference the XOM class objects. This is because the magic that makes JRuby understand that java.lang.String is a Java class, doesn’t work for nu.xom.Nodes, because nu is not a common Java package starter. So we use Java::nu.xom.Nodes, and everything works great.
Now, if we want our collections to behave more like Ruby arrays, then we need to mix Enumerable into our Java objects. Enumerable will not work unless the object has an each method that iterates over the collection, passing each iterated item to yield, in turn.
module XomCollectionExtensions include Enumerable # Implements each method as described in the Enumerable contract def each (0...size).each { |i| yield get(i) } end end
What I’ve done here is created my own mixin module. I’ve included Enumerable in my module, and I’ve created the each method. My each implementation just uses an end exclusive Range to iterate over the Nodes in the collection.
Now I can either re-open the Java class and include my module (as well as aliasing the append and get method)…
# modifications to the nu.xom.Nodes to make it behave more # like a Ruby Array class XomNodes include XomCollectionExtensions alias_method :[], :get alias_method :<<, :append end
… or I can send the include and alias_method messages to the class.
XomElements.send :include, XomCollectionExtensions XomElements.send :alias_method, :[], :get
If you’ve been following along, you should now be able to check out your handiwork:
nodes = XomNodes.new nodes << XomElement.new('first') nodes << XomElement.new('second') nodes.each { |n| p n.to_xml }
Also, if you’re interested in what else can be done with this sort of black magic (besides hacking up your Java objects at runtime), errtheblog has a some nice examples of using alias_method_chain. A welcome bit of warmth that was on this cold and snowy evening, I can tell you.





2 responses so far ↓
Brian Sam-Bodden // January 31, 2008 at 11:08 am
Thanks for this. Incidentally I was playing with JRuby and XOM. Created a little library that pretty much is a blatant copy of Jim Weirich’s Builder and implemented it on top on XOM.
Jay Lawrence // March 28, 2008 at 6:06 am
Nice stuff! We’re using JRuby a lot too … I will definitely be using these ideas to make our JRuby experience even more rewarding.
Leave a Comment