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.

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);
    }
}

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.

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());
    }
}

Here is a sample output from the test.

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

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.

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<Class> getClasses() {
                HashSet<Class> set = new HashSet<Class>();
                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());
    }
}

Here are the maven dependencies:

    
        
            com.sun.jersey
            jersey-client
            1.1.4.1
        
        
        
            org.apache.wink
            wink-client
            1.0-incubating
                
    

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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s