Monday, April 27, 2009

Ruby RJB: Confusion about Rjb::bind

Currently, I am working on an automated testing project that requires consuming webMethods services from an non-webMethods client. I like the productivity gain I get when working with Ruby so I want to build the "business" and presentation layers (the things that make up test cases) in Ruby, but I don't want to have to spend alot of time figuring out how to call WM services using SOAP with Ruby. The reason for this is because I already have a pretty good Java client that calls WM services just fine.

So my shortcut is to use RJB (or, Ruby Java Bridge) to be able to use the java WM client inside of my Ruby code. RJB uses JNI to to talk to the JVM (which is loaded by calling Rjb::load(classpath=.,jvmargs=[]) ) instead of taking the JRuby approach of compiling ruby code into the java bytecode that can be executed in the JVM.

The documentation on the RJB page ( rjb.rubyforge.org), while good enough to get you started, is rather sparse. One of the documented methods is Rjb::bind(obj,int), where obj is a ruby object that satisfies a Java interface, and int is the fully qualified name of a java interface, eg java.util.Map.

This allows you to bind a ruby object to an interface inside the JVM. The first impression I gleaned from this is that if you are calling a method on a Java object from inside Ruby, you need to have Ruby objects that satisfy the interfaces of the arguments to the method.

For instance, The Java object I am using in my Ruby code is com.gxs.wmclient.ServiceClient and specifically, the method I need to execute is: public HashMap runService(String url, String user, String pass, String serv, HashMap inputs). RJB handles translations of strings, so a Ruby string is automatically translated to a Java String. The HashMap at the end of the arg list is what I was scratching my head over. And it seemed to me, the way to do it was to implement a HashMap in Ruby and provide method definitions for all the Map interface methods. Then, in my ruby script that calls the WM Service, instantate a Ruby HashMap, bind it to java.util.Map with the Rjb::bind method, then pass it into the call to runService(). All of that looks something like this (I am leaving out the ruby HashMap):



def setup
#use the setup method to load the JAR files required for ServiceClient
@path = File.dirname(__FILE__) +"/../lib/wmjsonclient.jar;"
@path = @path + File.dirname(__FILE__) + "/../lib/client.jar;"
@path = @path + File.dirname(__FILE__) + "/../lib/enttoolkit.jar"
Rjb::load(@path)
end

def test_service_call_with_hashmap

#import the java class I want to use
wm_serv = Rjb::import("com.gxs.wmclient.ServiceClient")
#create a new instance of ServiceClient
client = wm_serv.new

#Create a new Ruby HashMap
input = HashMap.new()

#bind the HashMap to java.util.Map and add two objects to it
input = Rjb::bind(input,"java.util.Map")
input.put("num1","3")
input.put("num2","4")
assert_equal 2, input.size()

#create a hashmap to catch the outputs, bind this to
#java.util.Map as well
outputs = HashMap.new()
outputs = Rjb::bind(outputs,"java.util.Map")

#execute the runService method
outputs = client.runService("WMServer.inside.gxs.com","AUser","APassWrd",inputs);

s = outputs.get("value")
puts "s type: #{s._classname}"
assert_equal 3, outputs.size()
assert_equal "7", s.toString()

end


Unexpectedly (for me at least) this fails miserably:

NoMethodError: undefined method `put' for #<#<Class:0x2d48b48>:0x2cf1884>
C:/Documents and Settings/WhitenerR/RubymineProjects/JavaBridgeTests/tests/hash_map_test.rb:67:in `test_service_call_with_hashmap'


No 'put' method? The Map interface most definitely defines a put() method, and I have implmented it in my Ruby HashMap:

class HashMap < Hash
...
#stores an object o at key k in this hash map
def put(k,o)
self[k] = o
end
...
end


I looked and looked on the web, did alot of Google searching, read alot of Ruby forum posts, thumbed through my "Enterprise Integration with Ruby" book, and nothing. I had a hard time figuring out why the put() method isn't visible after I bind my Ruby object to the Map interface. The only explanation I can come up with is this:

NoMethodError: undefined method `put' for #<#<Class:0x2d48b48>:0x2cf1884>

The bind method appears to have wrapped a class of some kind around my HashMap class. I am guessing that this wrapper is something from the JVM, probably the Map interface. I am now trying to use this "interface" in my Ruby code as if it were a concrete object - but it isn't, and since it isn't, when it was created in the Ruby VM, it probably didn't come with any methods. If this is true, what is the point of having the bind operation?

Anyways, I am not getting paid to figure out the intricacies of Rjb::bind, I am getting paid to provide a solution. Here is how I got around this: instantiate java.util.HashMap objects to hold inputs and catch outputs and use those instead of trying to bind Ruby objects to interfaces inside the JVM. Here is what it looks like:


#this one no longer uses the Ruby hashmap. It is apparent
#that I don't need to pass ruby objects into the JVM, since I can
#instantiate Java objects in Ruby and then pass those in.
def test_service_call_with_hashmap

#import the java classes I want to use
wm_serv = Rjb::import("com.gxs.wmclient.ServiceClient")
j_hashmap = Rjb::import("java.util.HashMap")

#create a new instance of ServiceClient
client = wm_serv.new

#create a new java HashMap, and add some objects to it.
j_inputs = j_hashmap.new
j_inputs.put("num1","3")
j_inputs.put("num2", "4")
assert_equal 2, j_inputs.size()

#execute the runService method, notice I can just catch the HashMap output,
#I can use outputs in my ruby code just like its a ruby object!
outputs = client.runService("WMServer.inside.gxs.com","AUser","APassWrd","pub.math:addInts",j_inputs);

#get the output I am looking for from the service call
s = outputs.get("value")
puts "s type: #{s._classname}"
assert_equal 3, outputs.size()
assert_equal "7", s.toString()

end


And the results:

4 tests, 7 assertions, 0 failures, 0 errors
Test suite finished: 27.126 seconds

key1 => value1
key2 => value2
Context Established....
Connection Established with Integration Server....
JSON::
{
"num1": "3",
"num2": "4"
}
Executing Service> pub.math:addInts
Service invocation complete....
s type: java.lang.String
#

Process finished with exit code 0


Maybe one day I will figure out a use for binding Ruby objects to interfaces in the JVM, but it won't be today.

No comments:

Post a Comment