(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?
The two choices that seemed to work for me are both documented here. I didn't receive a response yet about whether using a ThreadLocal was fine or not, but I don't see why it shouldn't be.
My secoond method which was added to the original question was to go the route of the handler. While debugging the WS callout, I noticed that the invocationProperties map had the SOAP response as part of an internal packet structure within the responseContext object, but there appeared to be no way of getting to it. The ResponseContext was a set of name value pairs. However, when I read the source code for ResponseContext at this location, I saw that the code for the get method had a comment about returning null if it could not find an Application Scoped property, otherwise, it would read it from the packet invocationProperties, which seemed to be what I wanted. So I seached on how to set the scope on the key/value pair (Google: setting application-scope property for jaxws) that the context was introducing it low-and-behold, it was in the jax-ws spec that I referenced in the other thread.
I also did some reading about the Packet, https://jax-ws.java.net/nonav/jax-ws-20-fcs/arch/com/sun/xml/ws/api/message/Packet.html.
I hope this makes some sense for you. I was concerned that three wouldn't be anything to use JAXB against if the web service call resulted in a Soap FAULT, and I really wanted to log this packet, since it was being returned from a Payment Gateway which to this day has a number of undocumented results.
Good luck.
The solution was to use JAXB to convert the objects back to XML. I didn't really want to do this because it seems redundant to have the webservice client receive XML, convert it to a POJO, only to have that POJO converted back to XML, but it works.
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.