Using DB lookups and XQuery for mock services

This article describes how one could use the XQuery support introduced since 1.0-beta-3-b2, to generate mock REST services. It also describes how ETags could be set and checked against requests.

 

This article describes the sample configuration ultra-sample-108.xml and the associated unit test MockingWithXQueryTest. The configuration defines the XQuery support by defining an instance of the class "org.adroitlogic.ultraesb.core.helper.XQuerySupport" to the configuration. This is because the XQuery support has been separated from the core functionality as the required libraries could be rather large in size. To run this example, first get the Saxon HE 9.2 Jar from the AdroitLogic Maven repository here and save it into lib/optional.

The sample configuration uses a Java class sequence "samples.services.mock.XQueryMocker" for easier debugging that may help one to develop a configuration when starting out. Thus the crux of the configuration could be shown as below:

<u:proxy id="subscription-mock">
    <u:transport id="http-8280"/>
    <u:target>
        <u:inSequence>
            <u:class name="samples.services.mock.XQueryMocker"/>
        </u:inSequence>
    </u:target>
</u:proxy>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="org.apache.derby.jdbc.ClientDriver"/>
    <property name="url" value="jdbc:derby://localhost:1529/../modules/sample/target/unittestdb"/>
    <property name="username" value="admin"/>
    <property name="password" value="admin"/>
</bean>

<bean id="xq" class="org.adroitlogic.ultraesb.core.helper.XQuerySupport">
    <constructor-arg ref="fileCache"/>
</bean

The unit test expects a "SUBSCRIPTION" table to be available from the Database, and for a GET request with a "If-None-Match" header (i.e. ETag), checks the latest known ETag from the table. If the client tag matches the value on the database, a HTTP 304 - Not Modified response is returned. If the ETags mismatch, or was not sent by the client, a Spring SimpleJdbcTemplate is used to query the SUBSCRIPTION table, and extract the matching record as a Domain object "Subscription".

To invoke the XQuery "samples/resources/sample1.xq" defined as follows, we now move the expected variables into a Map from the subscription domain object, and invoke the XQuery.

declare variable $id as xs:string external;
declare variable $startdate as xs:string external;
declare variable $enddate as xs:string external;
declare variable $category as xs:int external;
declare variable $username as xs:string external;

<m:subscription xmlns:m="http://mock.samples">
  <m:id>{$id}</m:id>
  <m:startdate>{$startdate}</m:startdate>
  <m:enddate>{$enddate}</m:enddate>
  <m:category>{$category}</m:category>
  <m:username>{$username}</m:username>
</m:subscription>


The transformMessage() method expects a Message as a parameter, and writes the transformation output - expected as an XML document - as its payload.

public class XQueryMocker implements JavaClassSequence {

    // a Spring row mapper to make it easier to query the sample database and create a Domain object from a result row
    private final ParameterizedRowMapper<Subscription> mapper = new ParameterizedRowMapper<Subscription>() {
        public Subscription mapRow(ResultSet rs, int rowNum) throws SQLException {
            Subscription sub = new Subscription();
            sub.id = rs.getString("id");
            sub.startdate = rs.getTimestamp("startdate");
            sub.enddate = rs.getTimestamp("enddate");
            sub.category = rs.getInt("category");
            sub.username = rs.getString("username");
            sub.etag = rs.getString("etag");
            return sub;
        }
    };

    // life cycle methods not used in this example
    public void init(Configuration config) {}
    public void destroy() {}

    public void execute(Message msg, Mediation mediation) throws Exception {

        Message res = msg.createDefaultResponseMessage();
        String method = (String) msg.getMessageProperty(HttpConstants.METHOD);

        if ("GET".equals(method)) {

            // get etag from request, and query parameters
            String clientETag = msg.getFirstTransportHeader("If-None-Match");
            @SuppressWarnings("unchecked")
            Map<String, String> params = (Map<String, String>) msg.getMessageProperty(HttpConstants.QUERY_PARAM_MAP);

            if (clientETag != null) {
                SimpleJdbcTemplate t = new SimpleJdbcTemplate(mediation.getDataSource("dataSource"));
                String serverETag = t.queryForObject("SELECT ETAG FROM SUBSCRIPTION WHERE ID = ?", String.class, new Object[]{params.get("id")});

                if (clientETag.equals(serverETag)) {
                    mediation.sendResponse(msg, 304);
                    return;
                }
            }

            // if client did not send a if-none-match header, or if the etag sent did not match the latest on DB
            SimpleJdbcTemplate t = new SimpleJdbcTemplate(mediation.getDataSource("dataSource"));
            Subscription sub = t.queryForObject("SELECT * FROM SUBSCRIPTION WHERE ID = ?", mapper, params.get("id"));

            // convert the Domain object into a Map - to be passed into the XQuery evaluation
            Map<String, Object> vars = new HashMap<String, Object>();
            if (sub != null) {
                vars.put("id", sub.id);
                vars.put("startdate", sub.startdate.toString());
                vars.put("enddate", sub.enddate.toString());
                vars.put("category", sub.category);
                vars.put("username", sub.username);
            }

            // invoke the XQuery transformation from the XQuerySupport bean defined into the configuration
            XQuerySupport xq = mediation.getSpringBean("xq", XQuerySupport.class);
            xq.transformMessage(res, "samples/resources/sample1.xq", vars);
            if (sub != null) {
                res.addTransportHeader("ETag", sub.etag);
            }
            mediation.sendResponse(res, 200);

        } else if ("POST".equals(method)) {

            XQuerySupport xq = mediation.getSpringBean("xq", XQuerySupport.class);
            Map<String, Object> vars = new HashMap<String, Object>();

            if (msg.getCurrentPayload() instanceof DOMMessage) {
                vars.put("payload", (((DOMMessage) msg.getCurrentPayload()).getDocument()));
            } else {
                DOMMessage domMessage = new DOMMessage(msg.getCurrentPayload());
                vars.put("payload", domMessage.getDocument());
            }

            String[][] ns = {{"m0", "http://mock.samples/"}};
            String paid = mediation.extractAsStringUsingXPath(msg, "//m0:updateSubscription/m0:amountPaid", ns);
            // just a simple increment of the amount paid as the current balance
            vars.put("balance", Double.valueOf(paid) + 1550.33);

            // transform the message and return the result with a successful reply
            xq.transformMessage(res, "samples/resources/sample2.xq", vars);
            mediation.sendResponse(res, 200);
        }
    }

    // a simple Domain object that maps to the DB table of subscriptions
    static class Subscription {
        String id;
        Date startdate;
        Date enddate;
        int category;
        String username;
        String etag;
    }
}

We also show a sample POST request, where we transform the request payload into a suitable response by extracting request elements using XPath expressions within the XQuery definition "samples/resources/sample2.xq" as follows:

declare namespace m0="http://mock.samples/";
declare variable $payload as document-node() external;
declare variable $balance as xs:double external;

<m:confirmation xmlns:m="http://mock.samples">
  <m:id>{$payload//m0:updateSubscription/m0:id/child::text()}</m:id>
  <m:username>{$payload//m0:updateSubscription/m0:username/child::text()}</m:username>
  <m:currentBalance>{$balance}</m:currentBalance>
  <m:currency>{$payload//m0:updateSubscription/m0:currency/child::text()}</m:currency>
  <m:timestamp>{$payload//m0:updateSubscription/m0:dateOfPayment/child::text()}</m:timestamp>
</m:confirmation>


Using XQuery would be one of the easiest ways to transform XML request payloads to response XML payloads, although XSLT, Velocity or any scripting languages or custom libraries or code may also be used. Note that we pass the request payload as the variable "$payload" to the above XQuery, and a hypothetical "balance" value as a Double variable.

Running the example

As this requires setting up a sample database, one could refer to the source code of the MockingWithXQueryTest.java test case found under samples/src directory in the distribution, and execute it within your favorite IDE. We now ship IDE configurations for IntelliJ IDEA, Eclipse and NetBeans!

Note: Be sure to download the Saxon HE 9.2 JAR from the above location or the Saxon web site at SourceForge, and place it under lib/optional before you run the example. You may need to update the Eclipse/NetBeans projects to detect this new Jar.

A GET request for http://localhost:8280/service/subscription-mock?id=12345 with a valiv ETag header (i.e. "If-None-Match: 686897696a7c876b7e") will return a HTTP 304 response, and a request for the second sample subscription value http://localhost:8280/service/subscription-mock?id=67890 will return a full response generated via the XQuery.

A POST request for updateSubscription XML payload will return the XQuery output as the response with some of the elements filled from the request elements, and hypothetical values.