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.