| download source |
One of the fascinations about learning a new language is the new stuff. The transition from Ruby to Java isn't all that big a leap. But the ability to perform metaprogramming operations is vastly superior to Java's reflection API. This is, of course, mainly a feature of Ruby's ability to evaluate code at runtime vs. Java's compile-type mode. But unlike other runtime evaluation languages, Ruby has added a few capabilities that provide a great deal of flexibility. Classes can be extended and even created at runtime. Code can be injected at any point, much like anonymous inner classes can be added in Java. The big difference is that injection at runtime means code can be morphed on the fly! First, a confession. I'm somewhat of a newbie to the Ruby world. One of my great faults is that I tend to skip all the boring parts -- especially those that mimic languages I know -- and jump immediately to the exciting bits. Score one for enthusiasm, but the reader may wish to take this into consideration. As much as possible, I have mimicked the code of more experienced Rubyists, adding only the bits I could not rifle from elsewhere. My first exercise in Ruby was to solve a problem that often faces Java programmers, rendering XML to domain objects. This translation has been problematic for Java coders for some time. My initial experience with this type of translation was to work with code that manually converted XML to Java. Every time the XML changed, so did Java code. (And not to mention the fact that it was almost always brutally long and boring code, hard to debug and very prone to mistakes.) A number of frameworks appeared over the years, notably JAXB and recently XMLBeans, which have encapsulated the nasty code and made the translation task quite a bit easier. They all share one flaw, though. Every time the XML changes, the code needs recompilation and redeployment. Ruby permits you to define classes and methods at runtime. So for an XML->Domain Object translation, it is possible to change the XML and have the code respond without code changes or redeployment. Conceivably, it is even possible for it to react without having to restart the interpreter. It is able to pass entire code blocks as strings that can be evaluated at runtime. These strings can contain variables as well as valid Ruby code. For this article, I created a web service (well, actually just a plain XML file), that represents a database query. The XML contains a ResultSet element as the root with an identifying "name" attribute. The root contains a number of Row elements with data like
<CUSTOMER_NUM>2</CUSTOMER_NUM>
<DISCOUNT_CODE>M</DISCOUNT_CODE>
<ZIP>33055</ZIP>
<NAME>Livingston Enterprises</NAME>
<ADDR_LN1>9754 Main Street</ADDR_LN1>
<ADDR_LN2>P.O. Box 567</ADDR_LN2>
<CITY>Miami</CITY>
<STATE>FL</STATE>
<PHONE>305-456-8888</PHONE>
<FAX>305-456-8889</FAX>
<EMAIL>www.tsoftt.com</EMAIL>
<CREDIT_LIMIT>50000</CREDIT_LIMIT>
<LAST_SALE_DATE>1998-01-02</LAST_SALE_DATE>
<LAST_SALE_TIME>09:00:00</LAST_SALE_TIME>
Not much new here if you are used to working with either XML or databases. In a Java domain model, this data becomes an Object of some sort. You can do the same thing with Ruby code, creating a domain class for each different ResultSet type and attributes (and their public accessors) a la JavaBeans standard. As mentioned earlier, this gets a little brutal with multiple ResultSets variations. My plan with Ruby was to use metaprogramming to create classes, instantiate objects and set the data representing this XML. But, first, some details about the state of XML in Ruby. It is supported by a number of different packages. I chose the REXML package largely because it came with the Ruby distribution and also because I found a good article about it at XML.com. Seems like all you need to do it declare some straightforward code like
require 'rexml/document'
include REXML
file = File.new(path)
doc = Document.new(file)
@root = doc.root
@name = doc.root.attributes['className']
Then you can do things like this below, which sets up the column names in the ResultSet
index = 0
if (@columns.length == 0)
@root.elements[1].each_element do |column|
@columns[index]= column.name
index = index + 1
end
end
or
root.elements["Row/email"].text Yes, it supports XPath and, yes, it is very straightforward. At least, by XML standards. In my case, I decided to wrap access to the XML in a XMLResultSet class that would encapsulate all data access necessary, including creating the long-sough-after domain objects. r = XmlResultSet.new("c:/temp/CustomerList.xml")
data = r.object(12, r.name) ##return a row object..
The magic I was looking for occurs in the "object" method of the XmlResultset class. This is the code that would take a bunch of XML elements, turn them into class members and set the data appropriately.
puts "Create a row object"
str = ""
#wrap object value in quotes to makes sure isn't interpreted as another parameter to the method.
@root.elements[i].each_element {|column| str = str + column.name.downcase
+ " String.new(\"" + column.text + "\") \n" }
DataObject.load(str, className)
Each element of the row would have its identifying XML removed so that a String like <EMAIL>www.tsoftt.com</EMAIL> would be translated into Ruby code like email String.new("www.tsoftt.com")
This is valid Ruby code and means something like setEmail("www.tsoftt.com") in Java parlance. You could do without the String.new business if all Strings were nice enough to come without spaces, but this was not the case. The interpreter would think the method had two or more parameters. The thing to remember here is that we are passing code to the interpreter and hoping it can be evaluated and run. Sending arbitrary pieces of method-invoking code, however, requires that classes exist with these methods. In our case, as the program starts, this is not the case. Classes have to be built on the fly and methods have to be created before the data is set. To accomplish this the superclass of all Data classes contains a static method that a) creates a Class if it does not yet exist, b) creates a method of that class if it does not exist and finally c) sets the data
def self.load(code, className)
dataObject = self.class.create(className)
puts "Object type: " + dataObject.class.to_s
dataObject.instance_eval(code)
dataObject
end
The data object is created by adding a method to the Module class. We'll look at that in a moment. For now, the actual instance of the DataObject class is returned from this "create" method. Then the code we have amassed is passed to the object's "instance_eval" method. You will find several of these "eval" type methods in Ruby. They are the magic which allows code to be evaluated and run on the fly. Ok. Back to the Module class, which will instantiate the new Class, if it exists - or -- create if it does not.
begin
# instantiate the object.
object = eval %{#{className}.new }
rescue
#create code for the new class
eval <<-CODE_TEMPLATE
class #{className} < DataObject
end
object = #{className}.new
CODE_TEMPLATE
end
Here we use an eval-type method to attempt to create the class. The text #{className} is a variable that can be set at runtime, before passing to the eval method. This allows us to react to new ResultSets and even name changes without touching source code. When a particular class does not exist yet, an exception will be thrown. In the rescue clause of the above code, we then use eval to construct the new Class and then instantiate it. The << markers indicate a multi-line String that will terminate when the identifier is repeated. In our case we use CODE_TEMPLATE. You can use any identifier, though. Once the data is set, object instantiation is complete and we can then proceed to query the data data = r.object(12, r.name) ##return a row object.. p data p "Value of email" + data.email data = r.object(11, r.name) p data This gives us a result of Create a row object object is of class Module::Organization Object type: Module::Organization #<Module::Organization:0x2b1d280 @email="www.nymedia.com", @city="New York", @name="NY Media Productions", @discount_code="L", @fax="212-222-5600", @addr_ln2="Suite 562", @last_sale_time="09:30:00", @customer_num="409", @phone="212-222-5656", @addr_ln1="4400 22nd Street", @last_sale_date="1997-10-20", @state="NY", @zip="10095", @credit_limit="10000"> "Value of email www.nymedia.com" Create a row object object is of class Module::Organization Object type: Module::Organization #<Module::Organization:0x2b16620 @email="www.sparts.com", @city="Detroit", @name="Small Car Parts", @discount_code="N",@fax="313-788-7600", @addr_ln2="Suite 35", @last_sale_time="09:00:00", @customer_num="722", @phone="313-788-7682", @addr_ln1="52963 Outer Dr", @last_sale_date="1998-02-20", @state="MI", @zip="48124", @credit_limit="50000"< Voila! Domain objects! In a couple of dozens lines of code, we have re-created the base features of XML->JavaBeans translation. The code isn't the prettiest, admittedly, but there isn't much of it and... to be honest... I'm getting really fascinated by Ruby. Hopefully, there's more under the hood. ResourcesREXML: Processing XML in Ruby Excellent article on the nitty gritty of REXML.Digging into Ruby Symbols a Ruby metaprogramming crash course Creating DSLs with Ruby by Jim Freeze. Copyright (c)2006 Gervase Gallant gervasegallant at yahoo.com |