CXF – Using interceptors to provide simple, property-based response filters

An example of building a CXF interceptor to provide a lightweight, property driven mechanism for web service consumers to filter response sets to suit their needs.

Providing a web service interface to your data can open up a world of possible new applications and consumers for it. Unfortunately it can also open up a world of change requests too, as each consumer tries to satisfy their specific needs as efficiently and neatly as possible.

By way of a simple (if somewhat unimaginative) example, let’s take a simple JAX-WS based web-service which provides end-of-day (EOD) stock prices for the London Stock Exchange:-

@WebService(targetNamespace = "https://www.devsumo.com/quoteService")
public interface QuoteService {
    public @WebResult(name="quote") List<StockQuote>
        getAllPricesForDate(@WebParam(name="date") Date date);
    public @WebResult(name="quote") List<StockQuote>
        getAllPricesForTicker(@WebParam(name="ticker") String ticker);
}

Our service offers two neat and generic methods; one that provides EOD prices for all stocks on the market and another which provides a price history for an individual stock. Ideal for a heavyweight desktop application with local network access to our service and plenty of memory to cache results locally, not so ideal perhaps for a web-based client or tablet application which is forced to download a large amount of data for every request over a slower network even if it only wants a fraction of the results. Our StockQuote object has only eight properties but still yields around 400Kb of XML for the London market’s EOD prices alone. This sort of thing can quickly make a web-service unusably unresponsive for real-time consumers.

public class StockQuote {
    private String ticker;
    private String market;
    private Date date;
    private BigDecimal open;
    private BigDecimal close;
    private BigDecimal high;
    private BigDecimal low;
    private BigInteger volume;

    // Getters and Setters
}

The least imaginative way of addressing this is to provide bespoke query methods and filtering parameters for each use case. This can quickly result in a messy, inconsistent API and a behemoth of a code base which is difficult to test, maintain and develop efficiently.

A cleaner and more efficient approach is to provide some form of generic response filtering capability to our consumers, the undisputed daddy of which is allowing a consumer-supplied XSLT transform to run on the result before it is returned to them. This offers a massively powerful filtering mechanism for little investment on the server side and without contaminating the underlying business logic. It does have a number of drawbacks though. In particular it demands XSLT skills on the part of our consumers, each of their XSLTs will need to be carefully tested when the WSDL changes and the server still have to render potentially large responses as an XML model before the filtering can take place. Undisputed daddy it may be, but for many requirements it’s also a sledgehammer to crack a nut.

In simpler cases with simpler needs we can strike a balance between power and complexity in our filtering mechanism whilst still providing a generic solution by using a ightweight filter specification header processed by an outbound interceptor on the web-service side. This would, for example, allow a consumer to limit a response from our web-service to just the highest volume stocks by adding a header like this to their SOAP request:-

<soapenv:Envelope
        xmlns:quot="https://www.devsumo.com/quoteService" 
        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Header>
        <quot:resultsFilter>
            <comparison>GREATER_THAN</comparison>
            <property>volume</property>
            <compareAs>NUMERIC</compareAs>
            <value>100000000</value>
        </quot:resultsFilter>
    </soapenv:Header>
    <soapenv:Body>
        <quot:getAllPricesForDate>
            <date>2014-08-11</date>
        </quot:getAllPricesForDate>
    </soapenv:Body>
</soapenv:Envelope>

Here’s how we can do it.

Defining a results filter

We can get quite clever with our response filters, supporting regular expression matches, type-specific comparison rules and Boolean logic for combining them for example, however to start with we’ll create a simple exclusion filter that allows basic comparisons between a result-set property and a specific value:-

public class ResultsFilter {

    public enum Comparison {
        EQUAL, NOT_EQUAL, GREATER_THAN, LESS_THAN
    }

    public enum ComparisonType {
        NUMERIC, STRING
    }

    private Comparison comparison = null;
    private ComparisonType compareAs = null;
    private String value = null;
    private String property = null;

    // Getters and Setters

}

Our filter has four properties. The comparison property specifies whether we’re looking for equality, greater-than, less-than etc. whilst the compareAs property controls whether we’re doing a lexical or numeric comparison. Value holds the value the consumer wants to test against and property specifies the results-set field to compare against.

Our ResultsFilter object needs to appear in our WSDL so consumers can use it. Since it’s not part of any method signatures this won’t happen automatically for a code-first JAX-WS service but we can use the @XmlSeeAlso annotation on our web-service interface to make sure it gets added:-

@WebService(targetNamespace = "https://www.devsumo.com/quoteService")
@XmlSeeAlso(ResultsFilter.class)
public interface QuoteService {
    public @WebResult(name="quote") List<StockQuote>
        getAllPricesForDate(@WebParam(name="date") Date date);
    public @WebResult(name="quote") List<StockQuote>
        getAllPricesForTicker(@WebParam(name="ticker") String ticker);
}

Intercepting our results filter

We’ll use a CXF outbound interceptor to locate and process these request headers. We’ll need to make sure it runs before the response is marshalled to XML so we can strip objects from the result set first. The interceptor will only run on the outbound chain but we can use the Exchange object to get at the inbound request and find the headers that were sent in:-

public class ResultsFilterInterceptor 
        extends AbstractSoapInterceptor {

    private static final QName RESULTS_FILTER_XML_TYPE =
        new QName("https://www.devsumo.com/quoteService", "resultsFilter");

    public ResultsFilterInterceptor() {
        super(Phase.PRE_MARSHAL);
    }

    @Override
    public void handleMessage(SoapMessage message) throws Fault {
        SoapMessage inboundMessage =
            (SoapMessage)message.getExchange().getInMessage();
        List<Header> headers = inboundMessage.getHeaders();
        if(headers != null) {
            // TODO: locate an process
        }
    }
}

CXF doesn’t automatically marshall headers into Java objects for us, so we’ll need to get an appropriate marshaller before we try to process them. We’ll also need to pull out the raw response objects from the outbound message before we try to filter it.

@Override
public void handleMessage(SoapMessage message) throws Fault {
    SoapMessage inboundMessage =
        (SoapMessage)message.getExchange().getInMessage();
    List<Header> headers = inboundMessage.getHeaders();
    if(headers != null) {
        // Prepare a header marshaller for CXF's binding type
        Service service =
            ServiceModelUtil.getService(message.getExchange());
        DataReader<Node> dataReader =
            service.getDataBinding().createReader(Node.class);

        // Find the raw response payload
        Object payload = null;
        MessageContentsList messageContentsList =
            MessageContentsList.getContentsList(message);
        if(messageContentsList != null && 
                messageContentsList.size() > 0) {
            payload = messageContentsList.get(0);
        }
        if(payload != null) {
            // TODO: Process the headers
        }
    }
}

If we have a payload we now need to iterate through the headers and apply each one that matches our ResultFilter‘s qualified name. For each matching header we’ll call a helper class to apply that filter to the results, which we’ll come on to next:-

public void handleMessage(SoapMessage message) throws Fault {
    SoapMessage inboundMessage =
        (SoapMessage)message.getExchange().getInMessage();
    List<Header> headers = inboundMessage.getHeaders();
    if(headers != null) {
        // Prepare a header marshaller for CXF's binding type
        Service service =
            ServiceModelUtil.getService(message.getExchange());
        DataReader<Node> dataReader =
            service.getDataBinding().createReader(Node.class);

        // Find the raw response payload
        Object payload = null;
        MessageContentsList messageContentsList =
            MessageContentsList.getContentsList(message);
        if(messageContentsList != null && 
                messageContentsList.size() > 0) {
            payload = messageContentsList.get(0);
        }

        if(payload != null) {
            // Apply each filter instance to the response payload
            for(Header header : headers) {
                if(header.getName().equals(RESULTS_FILTER_XML_TYPE)) {
                    ResultsFilter resultsFilter =
                        (ResultsFilter)dataReader.read(
                            RESULTS_FILTER_XML_TYPE,
                            (Node)header.getObject(), ResultsFilter.class);
                    ResultsFiltering.applyFilter(resultsFilter, payload);
                }
            }
        }
    }
}
Applying our results filter

To keep this example simple we’ll only apply our filter rules to any top-level list we find in the response object. We’ll also make it “error-tolerant” by swallowing and logging exceptions rather than throwing errors back to the consumer:-

public class ResultsFiltering {
    public static void applyFilter(ResultsFilter filter, Object obj) {
        try {
            // Try to locate a list at the top-level of the response
            List filterableList = null;
            for(Method getter : obj.getClass().getMethods()) {
                if(List.class.isAssignableFrom(getter.getReturnType()) &&
                        getter.getParameterCount() == 0) {
                    filterableList =
                        (List)getter.invoke(obj, (Object[])null);
                    break;
                }
            }

            // If we have a list, apply the filter to each element
            if(filterableList != null) {
                // TODO: Apply to each list entry
            }
        } catch(Exception e) {
            // TODO: Log
        }
    }
}

If we have a list we need to iterate it safely (as we’ll be deleting entries). We’ll use the Apache Commons BeanUtils class to extract the property value from each result object as it both keeps our code light and copes with nested property specifications for us. We’ll create an appropriate Comparable for both sides of the test – using the plain strings for lexical comparisons and BigDecimal for anything numeric. Finally, if our comparison hits a match, we remove that element from the list:-

public static void applyFilter(ResultsFilter filter, Object obj) {
    try {
        // Try to locate a list at the top-level of the response
        List filterableList = null;
        for(Method getter : obj.getClass().getMethods()) {
            if(List.class.isAssignableFrom(getter.getReturnType()) &&
                    getter.getParameterCount() == 0) {
                filterableList = 
                    (List)getter.invoke(obj, (Object[])null);
            }
        }

        // If we have a list, apply the filter to each element
        if(filterableList != null) {
            int i = 0;
            while(filterableList.size() > i) {
                try {
                    // Use BeanUtils to pull the property out
                    String value = BeanUtils.getProperty(
                        filterableList.get(i++), filter.getProperty());

                    // Use BigDecimal for numerics, raw string otherwise
                    Comparable ruleValue = 
                        filter.getCompareAs().equals(ComparisonType.NUMERIC) ?
                            new BigDecimal(filter.getValue()) :
                            filter.getValue();
                    Comparable thisValue = 
                        filter.getCompareAs().equals(ComparisonType.NUMERIC) ?
                            new BigDecimal(value) :
                            value;

                    // Perform the approopriate comparison
                    boolean hit = false;
                    switch(filter.getComparison()) {
                        case EQUAL :
                            hit = ruleValue.compareTo(thisValue) == 0;
                            break;
                        case NOT_EQUAL :
                            hit = ruleValue.compareTo(thisValue) != 0;
                            break;
                        case GREATER_THAN :
                            hit = thisValue.compareTo(ruleValue) > 0;
                            break;
                        case LESS_THAN :
                            hit = 0 > thisValue.compareTo(ruleValue);
                            break;
                    }

                    // Remove if the filter applied
                    if(hit) {
                        filterableList.remove(--i);
                    }
                } catch(Exception e) {
                    // TODO: Log
                }
            }
        }
    } catch(Exception e) {
        // TODO: Log
    }
}

Finishing touches

All that remains is to add our new Interceptor class to the outbound interceptor chain in our Spring beans.xml file:-

<cxf:bus>
    <cxf:outInterceptors>
        <bean id="resultsFilterInterceptor" 
              class="com.devsumo.quoteservice.ResultsFilterInterceptor"/>
    </cxf:outInterceptors>
</cxf:bus>

Our filters are now applied and the sample request earlier to filter by volume now returns 5Kb rather than 400Kb.

As with the XSLT approach, there are drawbacks to this approach too. The main one is that our property specification relates to the underlying object model rather than the WSDL model exposed to the consumers. This could become an issue in cases where the XML bindings are heavily customised. There’s also a lot of scope to extend this mechanism – handling dates, timestamps and other types, regular expression matching, combinatorial logic, etc. and while this is a good thing in some respects we need to guard against building something as complicated as XSLT would have been for the consumers in the first place.

Our implementation is a little rough-and-ready and would undoubtedly benefit from such nice touches as null checking and error handling, but it serves to illustrate how easy it can be to introduce a generic filtering mechanism for heavy result sets; a mechanism which doesn’t contaminate or complicate the testing of the core business logic we’re exposing and which meets simple client needs in a simple and understandable way.

Leave a Reply

Your email address will not be published. Required fields are marked *