How can I pass data back from a SOAP handler to a webservice client?

Viewed 6442

(Following up on this question: Getting raw XML response from Java web service client)

I've got a SOAP message handler that is able to get the raw XML of a web service response. I need to get this XML into the webservice client so I can perform some XSL transformations on the response before sending it on its way. I'm having trouble figuring out a good way to get data from a SOAP handler that catches incoming messages, and makes the raw XML available to a generated (from a WSDL) web service client. Any ideas if this is even feasible?


I've come up with something like this:

public class CustomSOAPHandler implements javax.xml.ws.handler.soap.SOAPHandler<javax.xml.ws.handler.soap.SOAPMessageContext>
{
    private String myXML;
    public String getMyXML()
    {
        return myXML;
    }
    ...
    public boolean handleMessage(SOAPMessageContext context)
    {
        ...
        myXML = this.getRawXML(context.getMessage());
    }

    //elsewhere in the application:
    ...
    myService.doSomething(someRequest);
    for (Handler h: ((BindingProvider)myService).getBinding().getHandlerChain())
    {
        if (h instanceof CustomSOAPHandler )
        {
            System.out.println("HandlerResult: "+ ((CustomSOAPHandler )h).getMyXML());
        }
    }

In very simple tests, this seems to work. But this solution feels somewhat like a cheap hack. I don't like setting the raw XML as a member of the chain handler, and I have a gut feeling this violates many other best practices. Does anyone have a more elegant way of doing this?

4 Answers

Example of handler that passes out request / response message bodies:

public class MsgLogger implements SOAPHandler<SOAPMessageContext> {

    public static String REQEST_BODY = "com.evil.request";
    public static String RESPONSE_BODY = "com.evil.response";

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

    @Override
    public boolean handleMessage(SOAPMessageContext context) {
        SOAPMessage msg = context.getMessage();
        Boolean beforeRequest = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(32_000);
            context.getMessage().writeTo(baos);
            String key = beforeRequest ? REQEST_BODY : RESPONSE_BODY;
            context.put(key, baos.toString("UTF-8"));
            context.setScope(key, MessageContext.Scope.APPLICATION);
        } catch (SOAPException | IOException e) { }
        return true;
    }

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

    @Override
    public void close(MessageContext context) { }
}

To register handler and use preserved properties:

BindingProvider provider = (BindingProvider) port;
List<Handler> handlerChain = bindingProvider.getBinding().getHandlerChain();
handlerChain.add(new MsgLogger());
bindingProvider.getBinding().setHandlerChain(handlerChain);

Req req = ...;
Rsp rsp = port.serviceCall(req); // call WS Port

// Access saved message bodies:
Map<String, Object> responseContext = provider.getResponseContext();
String reqBody = (String) responseContext.get(MsgLogger.REQEST_BODY);
String rspBody = (String) responseContext.get(MsgLogger.RESPONSE_BODY);

TL;DR

Metro JAX WS RI docs says about MessageContext.Scope.APPLICATION property:

The message context object can also hold properties set by the client or provider. For instance, port proxy and dispatch objects both extend BindingProvider. A message context object can be obtained from both to represent the request or response context. Properties set in the request context can be read by the handlers, and the handlers may set properties on the message context objects passed to them. If these properties are set with the scope MessageContext.Scope.APPLICATION then they will be available in the response context to the client. On the server end, a context object is passed into the invoke method of a Provider.

metro-jax-ws/jaxws-ri/rt/src/main/java/com/sun/xml/ws/api/message/Packet.java contains property:

/**
 * Lazily created set of handler-scope property names.
 *
 * <p>
 * We expect that this is only used when handlers are present
 * and they explicitly set some handler-scope values.
 *
 * @see #getHandlerScopePropertyNames(boolean)
 */
private Set<String> handlerScopePropertyNames;

On other hand metro-jax-ws/jaxws-ri/rt/src/main/java/com/sun/xml/ws/client/ResponseContext.java is an implementation of Map with:

public boolean containsKey(Object key) {
    if(packet.supports(key))
        return packet.containsKey(key);    // strongly typed

    if(packet.invocationProperties.containsKey(key))
        // if handler-scope, hide it
        return !packet.getHandlerScopePropertyNames(true).contains(key);

    return false;
}

In SOAPHandler we can mark property as APPLICATION instead of default MessageContext.Scope.HANDLER:

/**
 * Property scope. Properties scoped as <code>APPLICATION</code> are
 * visible to handlers,
 * client applications and service endpoints; properties scoped as
 * <code>HANDLER</code>
 * are only normally visible to handlers.
 */
public enum Scope {APPLICATION, HANDLER};

by:

/**
 * Sets the scope of a property.
 *
 * @param name Name of the property associated with the
 *             <code>MessageContext</code>
 * @param scope Desired scope of the property
 * @throws java.lang.IllegalArgumentException if an illegal
 *             property name is specified
 */
public void setScope(String name,  Scope scope);

As an alternative, instead of putting request/response details into the soap context, (in my case it did not work), you can put it into the ThreadLocal. So you need SOAPHandler, that @gavenkoa described (ty), but add it to the ThreadLocal instance, instead of the soap context.

Related