Using a cxf interceptor to handle gzip compression in a ws:consumer

A recent client engagement set me the challenge of handling compressed SOAP requests, after some investigation on a lead suggested by one of our architects I successfully managed to build out a cxf configuration and custom java classes that are able to compress/decompress a request/response.

Steps

Step one:- the cxf configuration

My journey started with this support article and frankly it's quite light on details, but if you want the TL;DR version you need to place a valid cxf.xml under src/main/resources.

Your cxf need be no more complex than this to invoke the 2 java classes we touch on in the next step.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:cxf="http://cxf.apache.org/core"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd 
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean class="com.blogger.jackandmare.PossiblyCompressedInboundInterceptor" id="in"/>
<bean class="com.blogger.jackandmare.CompressingOutboundInterceptor" id="out"/>
  <cxf:bus>
    <cxf:inInterceptors>
      <ref bean="in"/>
    </cxf:inInterceptors>
    <cxf:inFaultInterceptors>
      <ref bean="in"/>
    </cxf:inFaultInterceptors>
    <cxf:outInterceptors>
      <ref bean="out"/>
    </cxf:outInterceptors>
    <cxf:outFaultInterceptors>
      <ref bean="out"/>
    </cxf:outFaultInterceptors>
  </cxf:bus>
</beans>

Step two:- custom java classes

The above configuration relies on 2 java classes, one for the inbound (so in this case the response from our SOAP endpoint) and the other for outbound processing (in this case the request we send to the SOAP endpoint).

Outbound Java

import java.io.*;
import java.util.*;
import java.util.zip.*;
import org.apache.cxf.interceptor.*;
import org.apache.cxf.message.*;
import org.apache.cxf.phase.*;

public class CompressingOutboundInterceptor extends AbstractPhaseInterceptor<Message>  {

    public CompressingOutboundInterceptor() {
        super(Phase.PRE_STREAM);
    }
    public void handleMessage(Message msg) throws Fault {
        OutputStream os = msg.getContent(OutputStream.class);
        try {
            GZIPOutputStream gos = new GZIPOutputStream( os );
            msg.setContent(OutputStream.class, gos);
        } catch (IOException e) {
            // TODO: you should handle this exception
        }
    }
}

Inbound Java

import java.io.*;
import java.util.*;
import java.util.zip.*;
import org.apache.cxf.interceptor.*;
import org.apache.cxf.message.*;
import org.apache.cxf.phase.*;

/**
 * Custom PhaseInterceptor that can be associated with a cxf configuration to
 * handle an incoming message that may or may-not be compressed using gzip.
 * 
 * If it starts with the GZIP member header bytes (@see http://www.gzip.org/zlib/rfc-gzip.html#file-format)
 * then this will wrap the message with a GZIPInputStream (and reset), 
 * otherwise the message is reset and being wrapped in a PushbackInputStream.
 */
public class PossiblyCompressedInboundInterceptor extends AbstractPhaseInterceptor<Message>  {

    private static final byte[] GZIP_SIGNATURE = new byte[] {(byte) 0x1f,(byte) 0x8b}; // GZip compressed files/streams will start with these two bytes (and importantly any regular XML payload will not)

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

    public void handleMessage(Message msg) throws Fault {
        // In order to handle a response (incoming) message that may or may not have been compressed using gzip we need to read the first 2 characters of the stream - see https://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped

        PushbackInputStream pb = new PushbackInputStream( msg.getContent(InputStream.class), 2);
        byte [] signature = new byte[2];

        try {
    int len = pb.read( signature ); //read the signature
            pb.unread( signature, 0, len ); //push back the signature to the stream
            if( Arrays.equals(signature,GZIP_SIGNATURE) ) { //check if matches standard gzip magic number
                GZIPInputStream gis = new GZIPInputStream( pb );
                msg.setContent(InputStream.class, gis);
            } else {
                msg.setContent(InputStream.class, pb);
            }
        } catch (IOException e) {
            // TODO: you should handle this exception too
        }
    }
}

Some light reading if you want to know more.

I freely confess i got the above working in no small part through trial and error so my understanding of how CXF really works is quite limited.

This stack overflow response was in fairness super-helpful, and really helped me make basic progress

I did find the RedHat manuals sort-of helpful, especially these two tables
Inbound message phases
Outbound message phases

Step three:- set the headers correctly (using a message property transformer).




This is fairly trivial, but important (and allows me to show the use of a more compact way todo this than a chain of property elements).  Add the "Message Properties" element prior to the Web Service Consumer (by all means give it a descriptive name) and configure the HTTP headers "Accept-Charset" and "Content-Encoding" as illustrated.


Or if you prefer to work in xml the following code snippet will help you.

  <message-properties-transformer doc:name="assert gzip request and request UTF-8 + gzip response">
    <add-message-property key="Accept-Charset" value="UTF8"/>
    <add-message-property key="Accept-Encoding" value="gzip"/>
    <add-message-property key="Content-Encoding" value="gzip"/>
  </message-properties-transformer>

Word to the wise.

If you note carefully in the above, there is no actual linkage between the cxf and the ws:consumer outlined.  That's because there isn't any, it gets picked up by magic :D, this is great until it isn't (e.g. multiple ws:consumers that need different handling).  At that point you are going to have to start expanding the java above to handle these eventualities.

Taking this further.

As a note, if anyone has a good public SOAP service that accepts requests & supplies responses respecting the headers above, i will happily set up a full example based on this.

Comments