SOAP over RESTy Client

JAX-RS runtime allows applications to supply entity providers which maps services between representations (via @Produces and @Consumes) and their Java types. The entity provider interfaces MessageBodyReader and MessageBodyWriter defines the contract that supports the conversion of a stream to a Java type and vice versa.

Lets write a simple JAXRS provider for handling a SOAPMessage on the client-side.

[sourcecode language=’java’]
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import javax.xml.soap.*;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

@Provider
@Consumes(MediaType.TEXT_XML)
@Produces(MediaType.TEXT_XML)
public class SoapProvider implements MessageBodyWriter, MessageBodyReader {
public boolean isWriteable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return SOAPMessage.class.isAssignableFrom(aClass);
}

public SOAPMessage readFrom(Class soapEnvelopeClass, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap stringStringMultivaluedMap, InputStream inputStream) throws IOException, WebApplicationException {
try {
MessageFactory messageFactory = MessageFactory.newInstance();
StreamSource messageSource = new StreamSource(inputStream);
SOAPMessage message = messageFactory.createMessage();
SOAPPart soapPart = message.getSOAPPart();
soapPart.setContent(messageSource);
return message;
} catch (SOAPException e) {
e.printStackTrace();
}
return null;
}

public long getSize(SOAPMessage soapMessage, Class aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return -1;
}

public void writeTo(SOAPMessage soapMessage, Class aClass, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap stringObjectMultivaluedMap, OutputStream outputStream) throws IOException, WebApplicationException {
try {
soapMessage.writeTo(outputStream);
} catch (SOAPException e) {
e.printStackTrace();
}
}

public boolean isReadable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return aClass.isAssignableFrom(SOAPMessage.class);
}
}
[/sourcecode]

I used Jersey client to invoke the Weather Web Service from CDYNE. You can use any wsdl2java tool (I used CXF) to generate the types from the WSDL. Jersey would use the registered provider to map the SOAPMessage to the request and response. And finally, we bind the SOAPBody to the WSDL type using JAXB.

[sourcecode language=’java’]
import com.cdyne.ws.weatherws.GetCityWeatherByZIPResponse;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.LoggingFilter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.soap.*;

public class RESTyClient {
public static void main(String[] args) throws Exception {
ClientConfig config = new DefaultClientConfig();
config.getClasses().add(SoapProvider.class);
Client c = Client.create(config);
c.addFilter(new LoggingFilter());

MessageFactory messageFactory = MessageFactory.newInstance();
SOAPMessage message = messageFactory.createMessage();
SOAPPart soapPart = message.getSOAPPart();
SOAPEnvelope envelope = soapPart.getEnvelope();
SOAPBody body = envelope.getBody();
SOAPElement bodyElement = body.addChildElement(envelope.createName(“GetCityWeatherByZIP”, “”, “http://ws.cdyne.com/WeatherWS/”));
bodyElement.addChildElement(“ZIP”).addTextNode(“59102”);
message.saveChanges();

WebResource service = c.resource(“http://ws.cdyne.com/WeatherWS/Weather.asmx”);

// POST the request
ClientResponse cr = service.header(“SOAPAction”, “\”http://ws.cdyne.com/WeatherWS/GetCityWeatherByZIP\””).post(ClientResponse.class, message);
message = cr.getEntity(SOAPMessage.class);

JAXBContext ctx = JAXBContext.newInstance(GetCityWeatherByZIPResponse.class);
Unmarshaller um = ctx.createUnmarshaller();
GetCityWeatherByZIPResponse response = (um.unmarshal(message.getSOAPPart().getEnvelope().getBody().extractContentAsDocument(), GetCityWeatherByZIPResponse.class)).getValue();
System.out.println(“City : ” + response.getGetCityWeatherByZIPResult().getCity());
System.out.println(“Temperature : ” + response.getGetCityWeatherByZIPResult().getTemperature());
}
}
[/sourcecode]

Here is a sample output from the test.

[sourcecode language=’xml’]
Dec 3, 2009 10:02:04 PM com.sun.jersey.api.client.filter.LoggingFilter log
INFO: 1 * Client out-bound request
1 > POST http://ws.cdyne.com/WeatherWS/Weather.asmx
1 > SOAPAction: “http://ws.cdyne.com/WeatherWS/GetCityWeatherByZIP”
1 >




59102


Dec 3, 2009 10:02:04 PM com.sun.jersey.api.client.filter.LoggingFilter log
INFO: 1 * Client in-bound response
1 < 200 1 < Content-Length: 762 1 < X-AspNet-Version: 2.0.50727 1 < X-Powered-By: ASP.NET 1 < Cache-Control: no-cache 1 < Pragma: no-cache 1 < Content-Type: text/xml; charset=utf-8 1 < Server: Microsoft-IIS/7.0 1 < Date: Fri, 04 Dec 2009 05:02:21 GMT 1 < Expires: -1 1 <




true
City Found
MT
Billings
Billings
3
Mostly Cloudy
16
51
SW23
30.11F






City : Billings
Temperature : 16
[/sourcecode]

In fact, this should work across the same way with any other JAX-RS Clients (I know there a quite a few). I have tried with the new kid on the block Apache Wink, which looks very interesting, still in incubation, but I am impressed with their documentation, as I hardly find useful docs with other projects in incubation stage. And the client APIs are similar to that of Jersey client, so it was fairly easy to write one using Wink Client. I will have to reserve another blog entry for exploring Wink’s USP.

[sourcecode language=’java’]
import com.cdyne.ws.weatherws.GetCityWeatherByZIPResponse;
import org.apache.wink.client.ClientConfig;
import org.apache.wink.client.ClientResponse;
import org.apache.wink.client.Resource;
import org.apache.wink.client.RestClient;

import javax.ws.rs.core.Application;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.soap.*;
import java.util.HashSet;
import java.util.Set;

public class WinkClient {
public static void main(String[] args) throws Exception {
MessageFactory messageFactory = MessageFactory.newInstance();
SOAPMessage message = messageFactory.createMessage();
SOAPPart soapPart = message.getSOAPPart();
SOAPEnvelope envelope = soapPart.getEnvelope();
SOAPBody body = envelope.getBody();
SOAPElement bodyElement = body.addChildElement(envelope.createName(“GetCityWeatherByZIP”, “”, “http://ws.cdyne.com/WeatherWS/”));
bodyElement.addChildElement(“ZIP”).addTextNode(“59102”);
message.saveChanges();

ClientConfig config = new ClientConfig();

// Create new JAX-RS Application
Application app = new Application() {
@Override
public Set> getClasses() {
HashSet> set = new HashSet>();
set.add(SoapProvider.class);
return set;
}
};
config.applications(app);

RestClient client = new RestClient(config);

// create the resource instance to interact with
Resource resource = client.resource(“http://ws.cdyne.com/WeatherWS/Weather.asmx”);

// issue the request
ClientResponse cr = resource.contentType(“text/xml”).post(ClientResponse.class, message);
message = cr.getEntity(SOAPMessage.class);

JAXBContext ctx = JAXBContext.newInstance(GetCityWeatherByZIPResponse.class);
Unmarshaller um = ctx.createUnmarshaller();
GetCityWeatherByZIPResponse response = (um.unmarshal(message.getSOAPPart().getEnvelope().getBody().extractContentAsDocument(), GetCityWeatherByZIPResponse.class)).getValue();
System.out.println(“Response : ” + response.getGetCityWeatherByZIPResult().getCity());
System.out.println(“Response : ” + response.getGetCityWeatherByZIPResult().getTemperature());
}
}
[/sourcecode]

Here are the maven dependencies:

[sourcecode language=’xml’]


com.sun.jersey
jersey-client
1.1.4.1


org.apache.wink
wink-client
1.0-incubating


[/sourcecode]

While there are better tools and frameworks which addresses the complexity of building SOAP messages, it was not a big deal to invoke web services from HTTP Clients, after all its just a HTTP POST.