Friday, May 11, 2012

SOAP/WSDL Wrangling

Went looking for how to do client-side SOAP in Java, given a WSDL file. Found a couple of tutorials on how to create the server side; not what I wanted. Down at the end, they mentioned "oh yeah, to test the server, write a client, and here's how to do that in Netbeans". We don't use Netbeans. Grrr.

Back to StackOverflow. Found mention of wsimport. Hey, I've already got that installed. Apparently, wsimport is part of JAX-WS, and Glassfish is the reference implementation. So how do I use it? Finally found docs for using wsimport to generate client-side stub classes, and sample client code to call it.

Initially, I thought running wsimport was as simple as wsimport foo.wsdl; doing that creates classfiles under the current directory. jar cf foo.jar com packs those up in a jar (minus the manifest; is that a problem?). Add that jar to your project, and start writing Java code with them.

It seems a typical JAX-WS pattern is to instantiate a Service class, and use it to get a Port class. From there, calls from the client to the server involve calling methods on the Port class, i.e.:

FreightRateService svc = new FreightRateService();
FreightRatePortType port = svc.getFreightRatePort();

FreightRateResponse frResp = port.processFreightRate( frReq, upsSec );

In this example, the frReq and upsSec objects are instantiated from other classes in the generated jars, that are used to pass data from the client to the server. frResp can then be examined for return data. It's actually not much more complicated than that.

Except!

Except, when you run wsimport on a local WSDL, the generated class files contain an absolute path to your WSDL file in them. Not sure why the class files aren't enough, but apparently the WSDL is also needed at runtime. Apparently there are a few different ways to go about getting the WSDL file loaded:
  1. Make sure the absolute path is correct and sufficient. This might be the case if you're building the WSDL jar on the same box that you're deploying on.
  2. Use wsimport's -wsdllocation parameter. This might work if you can put the WSDL file in the same place on all boxes (i.e. development and production) that you intend to run the system on.  In my case, I develop on Linux and deploy to Windows, so no dice.
  3. Use wsimport's -wsdllocation and -catalog parameter. Never got this to work, but saw a StackOverflow post that mentioned it, that was quite highly voted up.
  4. Pass in a URL to the Service constructor, with your own path. This is what I settled on for Suzuki (see UPSRating.java), as it lets us put the file wherever we want, and (in theory) put the location into a properties file. You also need to pass in a QName (whatever that is), which I copied verbatim from the generated Service source file (i.e. FreightRateService.java); looks like it points to the WSDL's xsd or dtd or somesuch, maybe.
One last thing to mention, that I never solved.  While working on options 2 and 3 up there, I tried just specifying a bare filename, i.e.-wsdllocation FreightRate.wsdl. On my Linux box, running under Eclipse, it was looking for the WSDL in my home directory, and when the file wasn't there, it said so with a nice, clear error message. Something like FileNotFoundException: /home/djaquay/FreightRate.wsdl. Polite. Then I deployed it to Windows, thinking/hoping that it'd tell me in like fashion where it was looking for the WSDL. But there, I got the following:

No wsdl metadata with the Service, cant create proxy! Try creating Service by providing a WSDL URL

And Google, for once, wasn't my friend.  No matches at all, outside of one lone forum post asking, essentially, WTF?, and getting no real answer.  So if anybody knows what's up with that error, leave a comment.  I ran out of time to get stuff working, and went with option 4, but I'd still like to know what that error is about.

So yeah, SOAP is nice in theory, and a hassle in practice.  Have fun.