Metaprogramming with Ruby: Mapping Java Packages Onto Ruby Modules

Metaprogramming lets you define new constructs in your programming language. In safety languages like Java, metaprogramming is not a standard feature, although it can be done. Not surprisingly, the reaction most Java developers have to metaprogramming typically ranges from "useless" to "catastrophic". I was once in that category. But in the words of Paul Graham, sometimes doing the right thing involves "changing the language to suit the problem". This is a powerful concept that, when used with care, can greatly reduce development time. The Ruby language is especially well-equipped for metaprogramming. This article will show a simple metaprogramming technique that extends Ruby so that Java packages are mapped onto to Ruby modules.

Prerequisites

This tutorial uses Ruby Java Bridge (RJB). RJB can be installed either on Windows or Linux using the RubyGems packaging mechanism.

Defining the Problem

The large amount of existing Java code makes RJB one of Ruby's most useful integration tools. RJB provides a lightweight mechanism for working with Java classes from Ruby, while simultaneously allowing for the use of C-extensions and everything else the C implementation of Ruby offers.

Let's say you'd like to do image processing with Java's ImageIO class. This could be accomplished with RJB via:

require 'rubygems'
require_gem 'rjb'
require 'rjb'

@ImageIO = Rjb::import 'javax.imageio.ImageIO'

@ImageIO.getReaderFormatNames # => an array of format names

Here, the instance variable ImageIO holds a reference to the Java class of the same name. This isn't too bad, but can we do better?

Imagine a situation in which many Java classes need to be imported. This would lead to many variable assignments like the one above. The resulting duplications of Java class names and variable names doesn't smell especially good, nor does it scale well. In addition, the large number of these variable assignments would add a lot of mental overhead when reading and writing the code.

What we'd really like is if Ruby had a way for us to map a Java package/class hierarchy onto Ruby module/class hierarchy. We could then forget about the differences between Ruby and Java, and just get to work. For example:

#...

jrequire 'javax.imageio.ImageIO'

#...

Javax::Imageio::ImageIO.getReaderFormatNames

A Solution

Our solution will involve translating nested Java package/class constructs into nested Ruby module/class constructs at runtime. We'll need the ability to create new module hierarchies in running code, one of the problems metaprogramming solves. We'll also have to deal with capitalization: Java package names are all lowercase, but Ruby module names start with a capital letter. So java.lang.System will become Java::Lang::System. By mapping the Java package namespace onto the Ruby module namespace, we'll reduce the odds of creating a Ruby/Java class name collision, such as with java.lang.String.

Let's create a small library to illustrate these points. The code will take advantage of Ruby's const_set method, which allows new constants (and therefore new modules and classes) to be defined at runtime. Save the following code into a file called java.rb:

require 'rubygems'
require_gem 'rjb'
require 'rjb'

module Kernel

  def jrequire(qualified_class_name)
    java_class = Rjb::import(qualified_class_name)
    package_names = qualified_class_name.to_s.split('.')
    java_class_name = package_names.delete(package_names.last)
    new_module = self.class

    package_names.each do |package_name|
      module_name = package_name.capitalize

      if !new_module.const_defined?(module_name)
        new_module = new_module.const_set(module_name, Module.new)
      else
        new_module = new_module.const_get(module_name)
      end
    end

    return false if new_module.const_defined?(java_class_name)

    new_module.const_set(java_class_name, java_class)

    return true
  end
end  

Usage

Using this library consists of requiring it, applying the new jrequire command, and manipulating the resulting class:

require 'java'
jrequire 'javax.imageio.ImageIO'

Javax::Imageio::ImageIO.getReaderFormatNames # => ["BMP", "jpeg", "bmp", "wbmp", "gif", "JPG", "png", "jpg", "WBMP", "JPEG"]

You can eliminate the need to use the fully-qualified module name by adding an include statement, just as you would with any other Ruby module:

require 'java'
jrequire 'javax.imageio.ImageIO'
include Javax::Imageio

ImageIO.getReaderFormatNames # => ["BMP", "jpeg", "bmp", "wbmp", "gif", "JPG", "png", "jpg", "WBMP", "JPEG"]

One Possible Variation

An interesting variation on the approach given here would be to override Ruby's require method itself to accept fully-qualified Java class names. Then something even more Rubyesque could be used:

require 'java'
require 'javax/imageio/ImageIO'

Javax::Imageio::ImageIO.getReaderFormatNames

Other Examples of Ruby-Java Metaprogramming

Ola Bini has written an article on JRuby metaprogramming that takes a slightly different approach than the one detailed here.

Conclusions

This tutorial has shown a simple and practical application of Ruby's built-in metaprogramming capabilities. The careful use of metaprogamming is a powerful way to reduce code complexity and build a more consistent programming environment. Look for more metaprogramming techniques to appear in future releases of the Ruby CDK library.