Friday, 25 February 2011

How to add information to a SOAP fault message with EJB 3 based web services

Are you building a Java web service based on EJB3?
Do you need to return a more significant message to your web service clients other that just the exception message or even worst the recurring javax.transaction.TransactionRolledbackException?
Well if the answer is YES to the above questions then keep reading...

The code in this article has been tested with JBoss 5.1.0 but it should (!?) work on other EJB containers as well
  1. Create a base application exception that will be extended by all the other exception, I will refer to it as MyApplicationBaseException .
    This exception contains a list of UserMessage, again a class I created with some messages and locale information
  2. You need to create a javax.xml.ws.handler.soap.SOAPHandler < SOAPMessageContext > implementation. Mine looks like this
    import java.util.Set;
    import javax.xml.bind.JAXBContext;
    import javax.xml.bind.JAXBException;
    import javax.xml.bind.Marshaller;
    import javax.xml.namespace.QName;
    import javax.xml.soap.SOAPException;
    import javax.xml.soap.SOAPFault;
    import javax.xml.soap.SOAPMessage;
    import javax.xml.ws.handler.MessageContext;
    import javax.xml.ws.handler.soap.SOAPHandler;
    import javax.xml.ws.handler.soap.SOAPMessageContext;
    
    import org.apache.commons.lang.exception.ExceptionUtils;
    
    public class SoapExceptionHandler implements SOAPHandler<SOAPMessageContext> {
     private transient Logger logger = ServiceLogFactory.getLogger(SoapExceptionHandler.class);
    
     @Override
     public void close(MessageContext context) { }
    
     @Override
     public boolean handleFault(SOAPMessageContext context) {
      try {
       boolean outbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
    
       if (outbound) {
        logger.info("Processing " + context + " for exceptions");
        SOAPMessage msg = ((SOAPMessageContext) context).getMessage();
        SOAPFault fault = msg.getSOAPBody().getFault();
        // Retrives the exception from the context
        Exception ex = (Exception) context.get("exception");
        if (ex != null) {
         // Add a fault to the body if not there already
         if (fault == null) {
          fault = msg.getSOAPBody().addFault();
         }
         // Get my exception
         int indexOfType = ExceptionUtils.indexOfType(ex, MyApplicationBaseException.class);
         if (indexOfType != -1) {
          ex = (MyApplicationBaseException)ExceptionUtils.getThrowableList(ex).get(indexOfType);
          MyApplicationBaseException myEx = (AmsException) ex;
          fault.setFaultString(myEx.getMessage());
          try {
           JAXBContext jaxContext = JAXBContext.newInstance(UserMessages.class);
           Marshaller marshaller = jaxContext.createMarshaller();
           //Add the UserMessage xml as a fault detail. Detail interface extends Node
           marshaller.marshal(amsEx.getUserMessages(), fault.addDetail());
          } catch (JAXBException e) {
           throw new RuntimeException("Can't marshall the user message ", e);
          }
         }else {
          logger.info("This is not an AmsException");
         }
        }else {
         logger.warn("No exception found in the webServiceContext");
        }
       }
    
      } catch (SOAPException e) {
       logger.warn("Error when trying to access the soap message", e);
      }
      return true;
     }
    
     @Override
     public boolean handleMessage(SOAPMessageContext context) {
      return true;
     }
    
     @Override
     public Set<QName> getHeaders() {
      return null;
     }
     
     
    }
    
    

  3. Now that you have the exception handler you need to register this SoapHandler with the EJB. To do that you'll need to create an Xml file in your class path and add an annotation to the EJB implementation class.
    The xml file :
    <?xml version="1.0" encoding="UTF-8"?>
       <jws:handler-chains xmlns:jws="http://java.sun.com/xml/ns/javaee">
    
         <jws:handler-chain>
        <jws:handler>
          <jws:handler-name>ExceptionHandler</jws:handler-name>
          <jws:handler-class>com.mycompany.utilities.ExceptionHandler</jws:handler-class>
        </jws:handler>
         </jws:handler-chain>
    
       </jws:handler-chains>
      

    and the EJB with annotation will be

    import javax.jws.HandlerChain;
      
      @Local(MyService.class)
      @Stateless
      @HandlerChain(file = "soapHandler.xml")
      @Interceptors( { MyApplicationInterceptor.class })
      @SOAPBinding(style = SOAPBinding.Style.RPC)
      @WebService(endpointInterface = "com.mycompany.services.myservice", targetNamespace = "http://myservice.services.mycompany.com")
      public final class MyServiceImpl implements MyService {
      
      // service implementation
      
      }
      
      

  4. To make sure all my exceptions have proper messages and that the exception is set in the SOAPMessageContext I use an Interceptor to wrap all the service methods and transform any exception to an instance of MyApplicationException
    The interceptor has a single method
    @AroundInvoke
      private Object setException(InvocationContext ic) throws Exception {
       Object toReturn = null;
       try {
        toReturn = ic.proceed();
       } catch (Exception e) {
        logger.error("Exception during the request processing.", e);
        //converts any exception to MyApplicationException
        e = MyApplicationExceptionHandler.getMyApplicationException(e);
        if (context != null && context.getMessageContext() != null) {
         context.getMessageContext().put("exception", e);
        }
        throw e;
       }
       return toReturn;
      }
      
  5. That's it! You're done.

2 comments:

Pat Considine said...

Hi there

I have tried to get your example working in JBoss 5.1.0 GA, without success.

Essentially we try:
{code}
@Resource
private WebServiceContext context;
try{
returnPayload = ctx.proceed();
}catch (Exception e){
context.getMessageContext().put("Exception", e);
throw e;
}
{code}

But we experience a known issue in JBoss 5.1.0:
http://community.jboss.org/message/589104

Is there a workaround you found for this to get your example working.

Thanks for your time

Pat Considine

mericano1 said...

Hi, There are two options..
As far as I remember the injection works in the EJB itself so if you have only one service you can just put the setException method in the EJB and annotated it with @AroundInvoke.

Other solution, better if you have many services is to use the JBoss implementation classes to get the context.

I remember searching in the classpath for implementations of WebServiceContext.
Unfortunately I don't have the source code anymore...

If you find out please post it back here, could be useful to other!

Thanks