Ruby on Rails: anyType, soap4r and handsoap to the rescue

Perl makes the easy things easy and the hard things possible. - Larry Wall

This sentiment has been applied to many different languages, frameworks and systems. It not surprising; it's the underlying goal of most software. Ruby on Rails is a little different. It makes the easy things extremely easy. But god help you if you want to get off the train in between stops.

After playing with rails a few months ago, I wanted to upgrade my toy project by getting some data from a SOAP web-service. Google pointed me to soap4r, which seems to be the common solution. However, I quickly ran into problems.

It seems that the WSDL I was consuming made judicious use of the anyType primitive, which Java uses to expose an Object parameter in a method. However, the remote method end-points assumed you were going to tell it at runtime what the type of the object you're passing is. In practice, it was an integer.

<?xml version="1.0" encoding="utf-8" ?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <n1:find xmlns:n1="http://apiservice.bullhorn.com/">
      <session>
        <client>rO0ABXNyACpjb20uYnVsbGhvcm4uZGF0YXNlcnZpY2UuYXBpLkFwaURhdGFDbGllbnQAAAAAAAAA
AQIACEoACmxhc3RBY2Nlc3NJAA5zdXBlckNsdXN0ZXJJZEwADWNvcnBvcmF0aW9uSWR0ABNMamF2
YS9sYW5nL0ludGVnZXI7TAAGZGJOYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAAMbWFzdGVyVXNl
cklkcQB+AAFMAA5wcml2YXRlTGFiZWxJZHEAfgABTAAGdXNlcklkcQB+AAFMAAp1c2VyVHlwZUlk
cQB+AAF4cAAAASPoSwbkAAAAAHNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFs
dWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAARmdAAJQlVMTEhPUk4xc3EAfgAE
AE2pkXNxAH4ABAAABmZzcQB+AAQAAADpc3EAfgAEAAAP3A==</client>
        <corporationId>1126</corporationId>
        <userId>233</userId>
      </session>
      <entityName>JobOrder</entityName>
      <id xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:int">67</id>
    </n1:find>
  </env:Body>
</env:Envelope>

The soap4r generated classes output a packet without the namespaces on the id tag, which resulted in the following exception.

java.lang.IllegalArgumentException: Provided id of the wrong type for class com.bullhorn.entity.job.JobOrder.
Expected: class java.lang.Integer, got class org.apache.xerces.dom.ElementNSImpl
 at org.hibernate.ejb.AbstractEntityManagerImpl.find(AbstractEntityManagerImpl.java:196)
 at com.bullhorn.dataservice.jpa.BhEntityManagerImpl.find(BhEntityManagerImpl.java:62)
 at com.bullhorn.dataservice.serviceImpl.BaseService.find(BaseService.java:29)
 ...
Caused by: org.hibernate.TypeMismatchException: Provided id of the wrong type for class com.bullhorn.entity.job.JobOrder.
Expected: class java.lang.Integer, got class org.apache.xerces.dom.ElementNSImpl
 at org.hibernate.event.def.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:109)
 at org.hibernate.impl.SessionImpl.fireLoad(SessionImpl.java:905)
 at org.hibernate.impl.SessionImpl.get(SessionImpl.java:842)
 at org.hibernate.impl.SessionImpl.get(SessionImpl.java:835)
 at org.hibernate.ejb.AbstractEntityManagerImpl.find(AbstractEntityManagerImpl.java:182)
 ... 48 more

You could rightly say that this was a Java problem, or at least a problem with the API of this web-service. It's a good illustration of why you should try to use primitives only for web-service parameters. However, in this case I could not control the web-service, so I needed to solve this in Ruby.

I briefly tried to download the WSDL and muck with it. SoapUI, by the way, has the ability to download a multi-part (think 50 parts) WSDL and save all the pieces locally. But I soon exhausted my expertise in hand-editing WSDL. Besides, ideally this needed to be done at runtime to support types other than integers.

Having "gone off the rails", I turned to a library called handsoap. Their philosophy is that you often need a higher level of control over the SOAP packets themselves.

...soap4r has problems. It's incomplete and buggy. If you try to use it for any real-world services, you quickly run into compatibility issues... Handsoap tries to do better by taking a minimalistic approach. Instead of a full abstraction layer, it is more like a toolbox with which you can write SOAP bindings. -troelskn

As promised, I did have to do the SOAP binding myself. But I also got the opportunity to do anything I wanted to the XML DOM object, including set these pesky required namespaces.

require 'handsoap'

Handsoap.http_driver = :httpclient
Handsoap::Service.logger = $stdout

class ApiService < Handsoap::Service
  endpoint API_SERVICE_ENDPOINT

  def on_create_document(doc)
    # register namespaces for the request
    doc.alias 'tns', 'http://apiservice.bullhorn.com/'
  end

  def on_response_document(doc)
    # register namespaces for the response
    doc.add_namespace 'ns', 'http://apiservice.bullhorn.com/'
  end

  def start_session!(state)
    soap_action = ''
    response = invoke('tns:startSession', soap_action) do |message|
      message.add "username", state[:username]
      message.add "password", state[:password]
      message.add "apiKey", state[:apiKey]
    end
    node = response/"//return"
    { :client => (node/"//client").to_s, :corporationId => (node/"//corporationId").to_s , :userId => (node/"//userId").to_s}
  end

  def find(state)
    soap_action = ''
    response = invoke('tns:find', soap_action) do |message|

      session = state[:session]

      message.add "session" do |i|
        i.add "client", session[:client]
        i.add "corporationId", session[:corporationId]
        i.add "userId", session[:userId]
      end

      message.add "entityName", state[:entityName]

      # ids are set to "anyType" in the WSDL, and our end-point enforces that these namespaces be in the post
      message.add "id", state[:id] do |i|
        i.set_attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
        i.set_attr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
        i.set_attr("xsi:type", "xs:int")
      end

    end
  end

end

PS - Installing handsoap was a pita. I could not get the curb gem, a ruby binding for the Linux curl utility, installed. So this example uses httpclient, instead.



I'm currently working at NerdWallet, a startup in San Francisco trying to bring clarity to all of life's financial decisions. We're hiring like crazy. Hit me up on Twitter, I would love to talk.

Follow @chase_seibert on Twitter