by Jason Gilman

JRubyConf 2011 was two weeks ago on August 3rd and 4th where I gave a talk entitled “Building the Magic Web Service”. My presentation covered how the NASA ECHO developers built a Ruby gem using existing Java libraries to make SOAP web service requests. One of the topics my talk covered was the use of metaprogramming in JRuby with Java objects. I’m going to detail more of those metaprogramming techniques in this blog. Before diving in you should check out the magic_web_service project on Github at https://github.com/Element84/magic_web_service. It’s not the actual code used on our project but it demonstrates the metaprogramming techniques detailed in this article.

The SOAP API

SOAP is an XML protocol for making programmatic requests over the Internet. The magic_web_service project demonstrates communicating with a SOAP API on the NASA ECHO system, an Earth Science metadata repository provided by NASA. The magic_web_service project demonstrates logging in as a guest and logging out.

Spring Web Services and JAXB are Java libraries that support making SOAP web service requests. JAXB is an XML marshaling library built into Java that comes with a Java code generator called XJC. XJC can be fed an XML schema that describes messages on a SOAP API and it will output Java objects that can generate and parse the XML to communicate with the API. It generates a Java class for each of the types defined in the XML Schema. Spring Web Services handles the HTTP communication with the API and uses JAXB to marshal the XML.

Accessing Java Code in Ruby

Before you begin creating and manipulating Java objects in Ruby you have to get access to the compiled Java class files. JRuby will make them available if the class or jar files are in the classpath or by requiring a jar file the same way you would require a Ruby file.

magic_web_service/lib/java_dependencies.rb

# Require the generated and compiled JAXB code to talk to the ECHO API
require 'echo_ws.jar'

After you’ve required the jar files you can access the Java classes in JRuby. There are a few different ways to do this. One way that prevents you from having to spell out the full java package every time is to include it in a module. The classes will then be scoped within a module for easy access.

magic_web_service/lib/java_dependencies.rb

# Make generated JAXB classes available within a Ws module.
module Ws
  include_package "authentication"
end

Scripting Java from Ruby

The JRuby Wiki shows many different ways to access Java objects using Ruby. The demo_direct_scripting.rb file in the magic_web_service project demonstrates logging in and logging out using Spring Web Services and JRuby.

demo_direct_scripting.rb

# 0. Setup the Spring WS classes
marshaller = Jaxb2Marshaller.new
marshaller.context_paths = "authentication"

message_factory = SaajSoapMessageFactory.new
# We call after_properties_set because SaajSoapMessageFactory is normally created in a spring
# context file.  Spring would call this after initializing the object.
message_factory.after_properties_set

service_template = WebServiceTemplate.new(message_factory)
service_template.marshaller = marshaller
service_template.unmarshaller = marshaller
service_template.default_uri = "https://testbed.echo.nasa.gov/echo-v10/AuthenticationServicePortImpl"

# 1. Create the JAXB request object
login_request = Ws::Login.new

# 2. Set the properties on the request object.  The properties are the arguments to the web service
# request.
login_request.username = "guest"
login_request.password = "mypassword"

# one of the properties is a complex object called ClientInformation.
client_info = Ws::ClientInformation.new
client_info.client_id = "magic web service"
client_info.user_ip_address = "127.0.0.1"
login_request.client_info = client_info

# 3. Send the request
response = service_template.marshal_send_and_receive(login_request)
# response is an instance of Ws::LoginResponse
token = response.result
puts "Logged in with token #{response.result}"

Applying Metaprogramming

These are Java objects we’ve been manipulating but JRuby still allows us to treat them like regular Ruby objects. That means a lot of the same rules apply. We can add methods at runtime, use reflection to list methods, and apply other metaprogramming techniques. The code above is much less verbose than the equivalent Java code but we can do even better in Ruby. The first step towards shortening the code is to come up with a goal. We’ll define the API that we’d like to use to make web service requests.

demo_magic_web_service.rb excerpt

class DemoMagicWebService
  include Services

  def run
    # Login as a guest
    # We need to pass client information to the api.  Any object will work through duck typing.
    client_info = OpenStruct.new
    client_info.client_id = "magic web service"
    client_info.user_ip_address = "127.0.0.1"

    token = authentication_service.login("guest", "mypassword", client_info)
    puts "Logged in with token #{token}"

    authentication_service.logout(token)
    puts "Logged out"
  end
end

The above code shows the API that’d we like. We want a module, Services, that when it’s included makes different service methods available, such as authentication_service. The authentication_service method returns an instance of the MagicWebService class that will dynamically handle the calls to web service operations.

The Services module uses define_method to dynamically add the methods that the module will make available. define_method takes the name of the method to add and a block for the method body. The ECHO API has 16 different services available. If we were only going to define one or two service methods we probably wouldn’t go to the trouble of using metaprogramming here.

services.rb

# Defines methods that can be used to return the different services defined on a web service API
module Services
  # An array of the different services that will be defined on an including class.
  SERVICES = [:authentication]

  # A hash of the service name to MagicWebService instances
  SERVICE_HASH = SERVICES.inject({}) do |hash, service|
    hash[service] = MagicWebService.new(service)
    hash
  end

  SERVICES.each do |service|
    # Define the accessor method for the MagicWebService
    define_method("#{service}_service") do
      SERVICE_HASH[service]
    end
  end
end

The MagicWebService class is where the real magic happens. It performs its feats of dynamic strength by using method_missing. method_missing is a method on Ruby objects that’s called when a method is invoked that isn’t defined. It normally raises a NoMethodError. MagicWebService overrides it to dynamically handle any different web service invocation. Notice that method_missing uses const_get on the Ws module to find the JAXB request class. Recall from earlier that the Ws module included all the JAXB generated Java classes. const_get looks up a constant by name and returns it.

magic_web_service.rb

class MagicWebService
  #  …

  # Handles all web service operation requests.  Converts the method name into a JAXB request
  # object.  Sets the arguments on the JAXB request object and then invokes the web service
  # operation.
  def method_missing(name, *args)
    jaxb_request_class = Ws.const_get(name.to_s.camelize)
    request = jaxb_request_class.new
    set_properties(request, args)
    response = @service_template.marshal_send_and_receive(request)
    if response.respond_to? :result
      response.result
    end
  end

  private

  # Sets the properties on the JAXB java object.
  # Properties is an array that matches the element order defined in the JAXB object
  def set_properties(java_obj, properties)
    JavaPropertySetter.new(java_obj).set_properties_from_array(properties)
  end
end

The JavaPropertySetter is responsible for setting properties on the generated Java objects. The MagicWebService method_missing will receive the arguments to the web service operation in the order defined in the XML Schema element. The JavaPropertySetter needs to set these arguments in the correct fields on the request object. It’s able to do this by using reflection on the generated JAXB object.

The generated JAXB objects contain Java annotations that tell JAXB how to map the object to XML. The @XmlType annotation field propOrder lists the elements of the Login message in order.

Login.java

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "username",
    "password",
    "clientInfo",
    "actAsUserName",
    "behalfOfProvider"
})
@XmlRootElement(name = "Login", namespace = "http://echo.nasa.gov/echo/v10")
public class Login {

    @XmlElement(namespace = "http://echo.nasa.gov/echo/v10", required = true)
    protected String username;
    @XmlElement(namespace = "http://echo.nasa.gov/echo/v10", required = true)
    protected String password;
    // ...

Java’s reflection API allows us to retrieve the annotation metadata from a class. This is possible in normal Java code. Ruby makes the code more concise than the Java version which is normally a chore to write. Here’s how the JavaPropertySetter does that in JRuby.

java_property_setter.rb

# Makes the JAXB Java annotation classes available.
module JaxbAnnotation
  include_package "javax.xml.bind.annotation"
end

# Later in class
java_obj_class = @java_object.class.java_class
annotation = java_obj_class.annotation(JaxbAnnotation::XmlType.java_class)
property_names = annotation.prop_order.map(&:to_s)

After the JavaPropertySetter has the property names in order it needs to get the types of the properties. Java is a statically typed language so it knows the types of all fields at runtime. The JavaPropertySetter uses reflection to get the type of each property.

property_names.each do |name|
  type = java_obj_class.java_method("get#{name.camelize}").return_type.to_s
  @property_names_to_types[name.underscore] = type
end

The JavaPropertySetter sets all of the values on each of the reflected properties. It converts the value into the appropriate type. When a simple type like strings or integers are encountered no conversion is done as JRuby automatically handles this. When a complex object is encountered the JavaPropertySetter creates a new instance of the target type and calls recursively into itself to handle setting properties on that type.

Conclusion

JRuby provides great benefits to developers coming to it from Ruby or from traditional Java backgrounds. Ruby developers can take advantage of the large collection of Java libraries and frameworks available. Traditional Java developers should look to JRuby as a suitable replacement for the Java programming language without having to leave the existing frameworks, libraries, and deployment schemes they already have in place. JRuby’s ability to take full advantage of the Ruby language, including metaprogramming, makes this especially appealing.

Resources