15

I have been fiddling around with server side interceptors on CXF. But is seems that it is not a trivial task to implement simple incoming and outgoing interceptors that give me a plain string containing the SOAP XML.

I need to have the plain XML in the interceptor so that I can use them for specific logging tasks. The standard LogIn & LogOut interceptors are not up to the task. Is anyone willing to share some example on how I could implement a simple incoming interceptor that is able to get the incoming SOAP XML and a outgoing interceptor to again get the SOAP XML?

Kirby
  • 15,127
  • 10
  • 89
  • 104
Marco
  • 15,101
  • 33
  • 107
  • 174

4 Answers4

22

Found the code for an incoming interceptor here: Logging request/response with Apache CXF as XML

My outgoing interceptor:

import java.io.OutputStream;

import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.io.CacheAndWriteOutputStream;
import org.apache.cxf.io.CachedOutputStream;
import org.apache.cxf.io.CachedOutputStreamCallback;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.Phase;

public class MyLogInterceptor extends LoggingOutInterceptor {

    public MyLogInterceptor() {
        super(Phase.PRE_STREAM);
    }

    @Override
    public void handleMessage(Message message) throws Fault {
        OutputStream out = message.getContent(OutputStream.class);
        final CacheAndWriteOutputStream newOut = new CacheAndWriteOutputStream(out);
        message.setContent(OutputStream.class, newOut);
        newOut.registerCallback(new LoggingCallback());
    }

    public class LoggingCallback implements CachedOutputStreamCallback {
        public void onFlush(CachedOutputStream cos) {
        }

        public void onClose(CachedOutputStream cos) {
            try {
                StringBuilder builder = new StringBuilder();
                cos.writeCacheTo(builder, limit);
                // here comes my xml:
                String soapXml = builder.toString();
            } catch (Exception e) {
            }
        }
    }
}
Community
  • 1
  • 1
annkatrin
  • 415
  • 4
  • 7
  • 1
    What's the advantage of the Callback? – Basil Mar 17 '15 at 18:37
  • 2
    The onClose() method in the callback is invoked after the output stream has been flushed and its data is available for you to retrieve. – annkatrin Mar 19 '15 at 15:12
  • MyLogInterceptor extends LoggingOutInterceptor, which in turn extends AbstractLoggingInterceptor where limit is declared. – annkatrin Dec 21 '15 at 15:30
  • 1
    @Cleankod, `limit` is used to optionally truncate the string. – Kirby Mar 29 '16 at 19:36
  • @annkatrin dear could I use this code to modify the outgoing message using your code ...thanks in advance – kikicoder Jan 22 '22 at 14:17
  • @annkatrin The class is great for getting the soap message. I have one more question, I have a request which is signed and encrypted. How do I get the message that is after signature but before encryption ? Thanks for your reply. – maoanz Jun 23 '22 at 13:12
  • @maoanz I have not worked with soap for years now, and unfortunately I do not remember how signing and encryption were configured. Sorry for the late reply. – annkatrin Aug 02 '22 at 10:34
8

I could not get the above solution to work for me. This is what I developed and hope it can help others:

My "incoming" interceptor:

import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingMessage;

public class MyCxfSoapInInterceptor extends LoggingInInterceptor {


    public MyCxfSoapInInterceptor() {
        super();
    }

    @Override
    protected String formatLoggingMessage(LoggingMessage loggingMessage) {
        String soapXmlPayload = loggingMessage.getPayload() != null ? loggingMessage.getPayload().toString() : null;

        // do what you want with the payload... in my case, I stuck it in a JMS Queue

        return super.formatLoggingMessage(loggingMessage);
    }
}

My "outgoing" interceptor:

import org.apache.cxf.interceptor.LoggingMessage;
import org.apache.cxf.interceptor.LoggingOutInterceptor;

public class MyCxfSoapOutInterceptor extends LoggingOutInterceptor {

    public MyCxfSoapOutInterceptor() {
        super();
    }

    @Override
    protected String formatLoggingMessage(LoggingMessage loggingMessage) {
        String soapXmlPayload = loggingMessage.getPayload() != null ? loggingMessage.getPayload().toString() : null;

        // do what you want with the payload... in my case, I stuck it in a JMS Queue

        return super.formatLoggingMessage(loggingMessage);
    }
}

Something I added to my spring framework application context XML (remember to define the two interceptors in the XML file too)...

    ...

    <cxf:bus>
        <cxf:inInterceptors>
            <ref bean="myCxfSoapInInterceptor"/>
        </cxf:inInterceptors>
        <cxf:inFaultInterceptors>
            <ref bean="myCxfSoapInInterceptor"/>
        </cxf:inFaultInterceptors>
        <cxf:outInterceptors>
            <ref bean="myCxfSoapOutInterceptor"/>
        </cxf:outInterceptors>
        <cxf:outFaultInterceptors>
            <ref bean="myCxfSoapOutInterceptor"/>
        </cxf:outFaultInterceptors>
    </cxf:bus>

    ...

Note, there are other ways to add the interceptors such as via annotations which will allow you to only intercept specific soap services. The above way of adding interceptors the the "bus" would intercept all your soap services.

dulon
  • 735
  • 8
  • 10
  • 1
    I did exactly what you suggest but my `loggingOutInterceptor` hasn't been traversed. For the `loggingInInterceptor` it works properly as expectation. Do you know why? Thanks. – cuongnguyen Dec 27 '17 at 09:53
5

I just want to share one more option, how to get incoming and outgoing messages together at the same time for some logging purpose, for example log requests and corresponding responses to database.

import javax.xml.namespace.QName;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Set;

public class CxfLoggingHandler implements SOAPHandler<SOAPMessageContext> {

private static final String SOAP_REQUEST_MSG_KEY = "REQ_MSG";

public Set<QName> getHeaders() {
    return Collections.EMPTY_SET;
}

public boolean handleMessage(SOAPMessageContext context) {
    Boolean outgoingMessage = (Boolean) context.get (MessageContext.MESSAGE_OUTBOUND_PROPERTY);
    if (outgoingMessage) {
        // it is outgoing message. let's work
        SOAPPart request = (SOAPPart)context.get(SOAP_REQUEST_MSG_KEY);
        String requestString = convertDomToString(request);
        String responseString = convertDomToString(context.getMessage().getSOAPPart());
        String soapActionURI = ((QName)context.get(MessageContext.WSDL_OPERATION)).getLocalPart();
        // now you can output your request, response, and ws-operation    
    } else {
        // it is incoming message, saving it for future
        context.put(SOAP_REQUEST_MSG_KEY, context.getMessage().getSOAPPart());
    }
    return true;
}

public boolean handleFault(SOAPMessageContext context) {        
    return handleMessage(context);
}

private String convertDomToString(SOAPPart soap){
    final StringWriter sw = new StringWriter();
    try {
        TransformerFactory.newInstance().newTransformer().transform(
                new DOMSource(soap),
                new StreamResult(sw));
    } catch (TransformerException e) {
        // do something
    }
    return sw.toString();
}
}

and then connect that handler with webservice

<jaxws:endpoint id="wsEndpoint" implementor="#myWS" address="/myWS" >
    <jaxws:handlers>
        <bean class="com.package.handlers.CxfLoggingHandler"/>
    </jaxws:handlers>
</jaxws:endpoint>
error1009
  • 151
  • 2
  • 9
  • It works perfectly if replacing `if (outgoingMessage) {` with `if ( ! outgoingMessage) {` – Ivano85 Feb 13 '18 at 11:44
  • Hm, it should be "if (outgoingMessage)". Because incoming message should be saved for future, and when we have outgoingMessage, then we are able to log the data, since we have an access to both: incoming and outgoing messages now. – error1009 Feb 14 '18 at 12:57
  • Sorry, you're right because this is an endpoint :P ... I used it client side, so the outgoingMessage was the reqest in my case and the incoming message the response. I appreciated it a lot, great solution ;-) – Ivano85 Feb 15 '18 at 13:37
  • I'm glad that it was useful for you :) – error1009 Feb 17 '18 at 14:27
  • What is `SOAP_REQUEST_MSG_KEY` ? – Salman Feb 19 '18 at 08:22
  • hm, forgot to mention it is in the code. SOAP_REQUEST_MSG_KEY just a String value (any), which used as a key in a context map. added it now. – error1009 Feb 20 '18 at 08:52
  • Thanks for this solution. Couple of questions - 1. Where do I add the `` XML part? 2. Is it possible to pass a database record Id in the `context` so that the response can be stored in the corresponding row? I have generated the client using wsdl2java. – semantic_c0d3r Aug 16 '21 at 14:36
  • 1. `jaxws:endpoint` should be added in the spring context xml. this was given for the xml-configuration :) 2. I used `context.put(SOAP_REQUEST_MSG_KEY, ...);` to put request-body. But you can put there whatever you want, it is just a Map. – error1009 Sep 06 '21 at 06:36
3

Example for writing the text to a StringBuffer, with hooks for capturing some custom properties and filtering of the request XML:

public class XMLLoggingInInterceptor extends AbstractPhaseInterceptor<Message> {

    private static final String LOCAL_NAME = "MessageID";

    private static final int PROPERTIES_SIZE = 128;

    private String name = "<interceptor name not set>";

    protected PrettyPrinter prettyPrinter = null;
    protected Logger logger;
    protected Level reformatSuccessLevel;
    protected Level reformatFailureLevel;

    public XMLLoggingInInterceptor() {
        this(LogUtils.getLogger(XMLLoggingInInterceptor.class), Level.INFO,  Level.WARNING);
    }

    public XMLLoggingInInterceptor(PrettyPrinter prettyPrinter) {
        this(LogUtils.getLogger(XMLLoggingInInterceptor.class), Level.INFO,  Level.WARNING);

        this.prettyPrinter = prettyPrinter;
    }

    public XMLLoggingInInterceptor(Logger logger, Level reformatSuccessLevel, Level reformatFailureLevel) {
        super(Phase.RECEIVE);
        this.logger = logger;
        this.reformatSuccessLevel = reformatSuccessLevel;
        this.reformatFailureLevel = reformatFailureLevel;
    }

    public XMLLoggingInInterceptor(PrettyPrinter prettyPrinter, Logger logger, Level reformatSuccessLevel, Level reformatFailureLevel) {
        this(logger, reformatSuccessLevel, reformatFailureLevel);
        this.prettyPrinter = prettyPrinter;
        this.logger = logger;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void handleMessage(Message message) throws Fault {

        if (!logger.isLoggable(reformatSuccessLevel)) {
             return;
        }

        InputStream in = message.getContent(InputStream.class);
        if (in == null) {
            return;
        }

        StringBuilder buffer;

        CachedOutputStream cache = new CachedOutputStream();
        try {
            InputStream origIn = in;
            IOUtils.copy(in, cache);

            if (cache.size() > 0) {
                in = cache.getInputStream();
            } else {
                in = new ByteArrayInputStream(new byte[0]);
            }

            // set the inputstream back as message payload
            message.setContent(InputStream.class, in);

            cache.close();
            origIn.close();

            int contentSize = (int) cache.size();

            buffer = new StringBuilder(contentSize + PROPERTIES_SIZE);

            cache.writeCacheTo(buffer, "UTF-8");
        } catch (IOException e) {
            throw new Fault(e);
        }

        // decode chars from bytes
        char[] chars = new char[buffer.length()];
        buffer.getChars(0, chars.length, chars, 0);

        // reuse buffer
        buffer.setLength(0);

        // perform local logging - to the buffer 
        buffer.append(name);

        logProperties(buffer, message);

        // pretty print XML
        if(prettyPrinter.process(chars, 0, chars.length, buffer)) {
            // log as normal
            logger.log(reformatSuccessLevel, buffer.toString());
        } else {
            // something unexpected - log as exception
            buffer.append(" was unable to format XML:\n");
            buffer.append(chars); // unmodified XML

            logger.log(reformatFailureLevel, buffer.toString());
        }
    }


    /**
     * Gets theMessageID header in the list of headers.
     *
     */
    protected String getIdHeader(Message message) {
        return getHeader(message, LOCAL_NAME);
    }

    protected String getHeader(Message message, String name) {
        List<Header> headers = (List<Header>) message.get(Header.HEADER_LIST);

        if(headers != null) {
            for(Header header:headers) {
                if(header.getName().getLocalPart().equalsIgnoreCase(name)) {
                    return header.getObject().toString();
                }
            }
        }
        return null;
    }        

    /**
     * Method intended for use within subclasses. Log custom field here.
     * 
     * @param message message
    */

    protected void logProperties(StringBuilder buffer, Message message) {
        final String messageId = getIdHeader(message);
        if(messageId != null) {
            buffer.append(" MessageId=");
            buffer.append(messageId);
        }
    }

    public void setPrettyPrinter(PrettyPrinter prettyPrinter) {
        this.prettyPrinter = prettyPrinter;
    }

    public PrettyPrinter getPrettyPrinter() {
        return prettyPrinter;
    }

    public Logger getLogger() {
        return logger;
    }

    public String getName() {
        return name;
    }

    public Level getReformatFailureLevel() {
        return reformatFailureLevel;
    }

    public Level getReformatSuccessLevel() {
        return reformatSuccessLevel;
    }

    public void setReformatFailureLevel(Level reformatFailureLevel) {
        this.reformatFailureLevel = reformatFailureLevel;
    }

    public void setReformatSuccessLevel(Level reformatSuccessLevel) {
        this.reformatSuccessLevel = reformatSuccessLevel;
    }

    public void setLogger(Logger logger) {
        this.logger = logger;
    }
}

For a fully working example, with output interceptors, see my CXF module on github.

ThomasRS
  • 8,215
  • 5
  • 33
  • 48
  • Hello, i understand that we are using `CachedOutputStream` to get a re-readable `inputStream` but why is the name of the class : (`CachedOutputStream`), it doesn't even have input word in it ? is it supposed to perform other major function OR my understanding is flawed. Also why can't we use `DelegatingInputStream` instead ? – varunsinghal65 Mar 04 '19 at 17:55
  • I suggest asking the CXF authors – ThomasRS Mar 04 '19 at 22:16
  • I have already sent a mail to CXF user group, no response :-( – varunsinghal65 Mar 05 '19 at 09:19