CXF – Using Handlers to process SOAP headers in a JAX-WS web service

When using SOAP headers to specify the non-functional, generic aspects of a JAX-WS web-service, handler classes can offer a more effective solution than header annotated method parameters while still retaining a high degree of portability across implementations. Here we look at a simple example of using a JAX-WS handler with Apache CXF.

SOAP headers are an ideal means of supplying non-functional request details, such as authentication credentials, transaction attributes or class of service information which we may also want to apply generically and consistently across all operations on a web-service (or indeed across multiple web-services). Many common use-cases are covered by open standards too, which helps make our web-services more interoperable.

Most web-service frameworks offer their own mechanisms for handling these ‘aspects’ of our web-services, such as Apache CXF’s powerful and flexible Interceptor chains. Typically these implementation specific mechanisms give us more functionality, configurability and control when compared with what the JAX-WS spec offers, but leveraging them does more tightly bind us into our choice of implementation.

So here we’re going to stick with the JAX-WS standard and look at two ways of accessing SOAP headers – first using header annotated parameters and then with a Handler class.

Using header parameters

Let’s kick off with a simple example service for sending messages, which we’ve already annotated to be exposed as a JAX-WS web-service:-

@WebService(targetNamespace = "http://services.devsumo.com/cxfMessenger/v001")
public interface CXFMessenger {
    @WebMethod
    String sendMessage(
         @WebParam(name="recipient") String recipient,
         @WebParam(name="messageContent") String messageContent);
}

Let’s say we’re working a requirement to add a Class of Service option to this service. Thinking ahead we might need to specify other service options in the future so we decide to introduce a generic ServiceOptions class with a property for our class of service parameter:-

public enum ClassOfService {
    LOW,
    NORMAL,
    URGENT
}

public class ServiceOptions {

    private ClassOfService classOfService;

    public ServiceOptions() {
    }

    public void setClassOfService(ClassOfService classOfService) {
        this.classOfService = classOfService;
    }
    public ClassOfService getClassOfService() {
        return classOfService;
    }
}

The simplest option here is to just add our ServiceOptions object as a plain method parameter:-

@WebService(targetNamespace = "http://services.devsumo.com/cxfMessenger/v002")
public interface CXFMessenger {
    @WebMethod
    String sendMessage(
        @WebParam(name="serviceOptions") ServiceOptions serviceOptions,
        @WebParam(name="recipient") String recipient,
        @WebParam(name="messageContent") String messageContent);
}

While this works fine, it does put our serviceOptions object in the SOAP Body, and for this non-functional service ‘aspect’ we’d prefer it to be an optional header element.

Fortunately, all we need to do to achieve this is add header=true to our @WebParam annotation:-

@WebService(targetNamespace = "http://services.devsumo.com/cxfMessenger/v003")
public interface CXFMessenger {
    @WebMethod
    String sendMessage(
        @WebParam(name="serviceOptions", 
                  header=true) ServiceOptions serviceOptions,
        @WebParam(name="recipient") String recipient,
        @WebParam(name="messageContent") String messageContent);
}

Our generated WSDL now includes serviceOptions as a <soap:header/> element in our operation and it yields the sort of SOAP requests we ‘re looking for:-

<soapenv:Envelope
        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:v003="http://services.devsumo.com/cxfMessenger/v003">
    <soapenv:Header>
        <v003:serviceOptions>
            <classOfService>NORMAL</classOfService>
        </v003:serviceOptions>
    </soapenv:Header>
    <soapenv:Body>
        <v003:sendMessage>
            <recipient>joe.bloggs@devsumo.com</recipient>
            <messageContent>A test message</messageContent>
        </v003:sendMessage>
    </soapenv:Body>
</soapenv:Envelope>

All well and good but there are clearly a few drawbacks to this approach. To kick off with, we’re going to be applying our ServiceOptions header generically to all our operations and doing it in the above way means we physically need to add it to each operation signature as our service grows. That’s tedious in itself but can become a greater issue when we’re trying to expose existing business interfaces as web-services. It can also become exceptionally messy if we want to handle lots of different headers. Secondly we’re going to have to hook the processing of this parameter into each and every operations. Finally the XML header is going to be tied to our service namespace, which doesn’t work too well when we want to use open standard schemas for our header parameters.

The JAX-WS Handler alternative

The main JAX-WS alternative to header parameters is to use a Handler class. In a nutshell a Handler class exposes a handleMessage operation which, if added to the handler chain for a web-service, will get invoked whenever a request comes in.

To kick off we’ll revert our operation to remove the explicit header parameter as we’re now going to look for it and process it as part of the handler chain:-

@WebService(targetNamespace = "http://services.devsumo.com/cxfMessenger/v004")
public interface CXFMessenger {
    @WebMethod
    String sendMessage(
        @WebParam(name="recipient") String recipient,
        @WebParam(name="messageContent") String messageContent);
}

For our simple example though this poses an immediate problem. How do we get our ServiceOptions class into our WSDL if it’s not referenced on the interface? In this case using @XmlSeeAlso is the simplest way to make sure it gets included:-

@WebService(targetNamespace = "http://services.devsumo.com/cxfMessenger/v004")
@XmlSeeAlso(ServiceOptions.class)
public interface CXFMessenger {
    @WebMethod
    String sendMessage(
        @WebParam(name="recipient") String recipient,
        @WebParam(name="messageContent") String messageContent);
}

However, it won’t necessarily ensure we can unmarshall it:-

<soap:Fault>
    <faultcode>soap:Server</faultcode>
    <faultstring>javax.xml.bind.UnmarshalException:
        unexpected element 
        (uri:"http://services.devsumo.com/cxfMessenger/v004",
        local:"serviceOptions"). 
        Expected elements are
        &lt;{http://services.devsumo.com/cxfMessenger/v004}sendMessage>,
        &lt;{http://services.devsumo.com/cxfMessenger/v004}sendMessageResponse>
    </faultstring>
</soap:Fault>

JAXB may know about our serviceOptions element, but it isn’t set up to process it as a root element. Adding an @XmlRootElement annotation to our ServiceOptions class sorts this out for us:-

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class ServiceOptions {
    // …
}

Building a JAX-WS Handler

Now we can build a handler class to process our header for us. We need to create a new class which implements the SOAPHandler interface (and make sure that we implement the JAX-WS one, not the old JAX-RPC one):-

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;

public class ServiceOptionsHandler
        implements SOAPHandler<SOAPMessageContext> {

    private static final QName HEADER_TYPE = 
            new QName("http://services.devsumo.com/cxfMessenger/v004", 
                      "serviceOptions");

    private static final Set<QName> HEADER_TYPES =
            new HashSet<QName>(Arrays.asList(new QName[]{HEADER_TYPE}));

    @Override
    public void close(MessageContext messageContext) {
    }

    @Override
    public boolean handleMessage(SOAPMessageContext messageContext) {
        // TODO: Here's where we'll process our header
        return true;
    }

    @Override
    public Set<QName> getHeaders() {
        return HEADER_TYPES;
    }

    @Override
    public boolean handleFault(SOAPMessageContext messageContext) {
        return true;
    }
}

Our framework handler takes care of all the boilerplate stuff we need to worry about; we’ve implemented all required methods of the Handler interface and set both handleMessage and handleFault to return true so they don’t stop normal processing from taking place. We’ve also added a constant for the QName of our header type – we’ll be needing that shortly and it also let’s us return something meaningful for getHeaders.

Now we can start to build out our handleMessage method to locate and process our serviceOptions header. The SOAPMessageContext class has a promising getHeaders method which will take that QName constant we created earlier and return the a matching object if the header was found in the request:-

Object[] getHeaders(QName header, JAXBContext context, boolean allRoles)

Unfortunately it also requires a JAXBContext through which it can unmarshall our object. The most portable solution (and indeed our only option if we’re using CXF with a data binding other than JAXB) will be to create our own JAXB context for our header object.

First of all we need to  make sure our ServiceOptions class is annotated with the right namespace:-

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(namespace="http://services.devsumo.com/cxfMessenger/v004")
public class ServiceOptions {
    // …
}

Then we’ll create a JAXBContext in our handler’s constructor for just this class:-

private JAXBContext jaxbContext = null;

public ServiceOptionsHandler() {
    try {
        jaxbContext = JAXBContext.newInstance(ServiceOptions.class);
    } catch(JAXBException eJaxb) {
        eJaxb.printStackTrace();
    }
}

And then we just pass it to getHeaders in our handleMessage function:-

@Override
public boolean handleMessage(SOAPMessageContext messageContext) {
    Object[] matchingHeaders = messageContext.getHeaders(HEADER_TYPE, 
            jaxbContext, true);
    if(matchingHeaders != null && matchingHeaders.length == 1) {
        // TODO: Process the header
    }
    return true;
}

If we are using CXF with its default JAXB binding and we’re happy to sacrifice a little portability, we could instead just hook our handler into the web-service JAXBContext. This requires us to cast our SOAPMessageContext so we can get at the CXF specifics and then dig down to the associated Service where we can pull out the DataBinding implementation (which we presume to be a JAXBDataBinding):-

@Override
public boolean handleMessage(SOAPMessageContext messageContext) {
    WrappedMessageContext wrappedMessageContext =
            (WrappedMessageContext)messageContext;
    Service service = ServiceModelUtil.getService(
            wrappedMessageContext.getWrappedMessage().getExchange());
    JAXBContext jaxbContext =
            ((JAXBDataBinding)service.getDataBinding()).getContext();
    Object[] matchingHeaders = messageContext.getHeaders(HEADER_TYPE, 
        jaxbContext, true);
    if(matchingHeaders != null && matchingHeaders.length == 1) {
        // TODO: process our header
    }
    return true; 
}

Finishing touches

All that remains now is to add the Handler to the chain for our service and it’ll fire whenever an incoming request arrives. I’m using Spring here so we just need to add a handler bean to the <jaxws:handlers/> child of the <jaxws:endpoint/> in my beans.xml file:-

<jaxws:endpoint 
        id="cxfMessengerV004"
        implementor="com.devsumo.cxfmessenger.v004.CXFMessengerImpl"
        address=" /cxfMessenger/V004">
    <jaxws:handlers>
        <bean id="serviceOptionsHandler"
              class="com.devsumo.cxfmessenger.v004.ServiceOptionsHandler"/>
    </jaxws:handlers>
</jaxws:endpoint>

Our handler doesn’t actually do anything at the moment of course, so by way of closure and to prove it’s working okay, we’ll stash the ServiceOptions object in our message context and access it from our service implementation:-

@Override
public boolean handleMessage(SOAPMessageContext messageContext) {
    WrappedMessageContext wrappedMessageContext =
            (WrappedMessageContext)messageContext;
    Service service = ServiceModelUtil.getService(
            wrappedMessageContext.getWrappedMessage().getExchange());
    JAXBContext jaxbContext =
            ((JAXBDataBinding)service.getDataBinding()).getContext();
    Object[] matchingHeaders =
            messageContext.getHeaders(HEADER_TYPE, jaxbContext, true);
    if(matchingHeaders != null && matchingHeaders.length == 1) {
        messageContext.put(ServiceOptions.class.getName(), matchingHeaders[0]);
    }
    return true;
}

It’s worth noting here that unlike CXF’s interceptors, which are configured specifically for inbound and outbound chains, our JAX-WS handler will be called twice. Once for the inbound request and once when the response is returned (which may be either handleMessage or handleFault). In our simple example the response will never have a matching header so we can ignore the second invocation for now, but in more operationally expensive cases, or cases where the same type could be returned, we’d need to code defensively for that case.

Finally we look for our stashed ServiceOptions object in our service implementation:-

public class CXFMessengerImpl implements CXFMessenger {
    @Resource
    protected WebServiceContext wsContext;

    private ServiceOptions retrieveServiceOptions() {
        WrappedMessageContext wrappedMessageContext =
                (WrappedMessageContext)wsContext.getMessageContext();
        return (ServiceOptions)(wrappedMessageContext.
                getWrappedMessage().getContextualProperty(
                            ServiceOptions.class.getName()));
    }

    @Override
    public String sendMessage(String recipient, String messageContent) {
        ServiceOptions serviceOptions = retrieveServiceOptions();
        System.out.println("Sending " + (serviceOptions != null ?
                 serviceOptions.getClassOfService() : "") +
                 " message \"" + messageContent + "\" to \"" + recipient);
        // …
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *