John D'Emic's blog about programming, integration, system administration, etc...

Friday, April 10, 2009

Mule SAAJ Transport

I've recently been working on a couple of projects that use complex SOAP API's. One of these API's is specified with a 1.5 megabyte WSDL along with something like 50 megabytes worth of Axis generated stub classes. Since I only needed to use a small subset of the WSDL's methods, I wasn't thrilled with dealing with the WSDL or the Axis stubs directly. As we're using Mule, this would have involved using either a CXF WSDL outbound-endpoint or maybe using the stub classes in a component.

Since I knew what the SOAP payloads look like (the SOAP body content), what I really wanted to do was just build this XML and pass it to an endpoint. It would also be nice if this endpoint dynamically set the SOAP message headers, extracted the SOAP body from the response and applied the response transformers (and perhaps got me a beer.)

I didn't see an obvious way to do with the CXF transport, so I took a stab at implementing such a transport myself. I had used SAAJ in a web-services proxy project I worked on last year and it seemed like a good fit. As such, I present the SAAJ-Transport. You can currently use it to pass arbitrary XML that is used as the SOAP body in messages sent to a SOAP endpoint. The endpoint handles constructing the SOAP message for you, adding the headers and extracting the SOAP body from the response (it won't get you a beer...yet.) Here's an example using a chaining-router to send a SOAP message and send the response to a VM endpoint.


<outbound>
<chaining-router>
<saaj:outbound-endpoint address="${service.url}" synchronous="true">
<transformers>
<transformer ref="templateToRequest"/>
<mulexml:xml-to-dom-transformer returnClass="org.w3c.dom.Document"/>
<saaj:document-to-soap-message-transformer/>
<saaj:mime-header-transformer key="Cookie" value="#[header:SERVICE_SOAP_SESSION_ID]"/>
</transformers>
</saaj:outbound-endpoint>
<vm:outbound-endpoint path="service.dispatcher">
<transformers>
<saaj:soapbody-to-document-transformer/>
<mulexml:dom-to-xml-transformer returnClass="java.lang.String"/>
</transformers>
</vm:outbound-endpoint>
</chaining-router>
</outbound>


The "document-to-soap-message-transformer" takes an org.w3c.dom.Document, transforms it to a SAAJ SOAPMessage and uses SAAJ to invoke the web-service. The mime-header-transformer adds a MIME header to the message (in this case a Cookie). Existing properties on the MuleMessage will be added the the SOAP header. When the response is received, the transport will extract out the SOAPBody and return it as the synchronous response, as well as set any SOAP headers as properties on the MuleMessage. In this case, the response is transformed back to a Document, then to a String, then finally passed out on the VM endpoint.

I'm hoping next week to get full documentation and examples up on the MuleForge page. I'm also planning to work on receiver functionality. This would allow you to receive SOAP messages on an inbound-endpoint and have their bodies extracted, headers set as Mule properties, etc. I'm still working on getting the distribution together. For now you'll need to checkout the source and use "mvn clean package" to build the jar or "mvn clean install" to get them into your local repository.

Tuesday, March 3, 2009

Mule, Smooks and Nagios

I've been working on upgrading our integration infrastructure on and off since the new year.  This began with the OpenMQ migration I previously blogged about and was followed by upgrading our Mule 1.4.3 services to Mule 2.x.  In addition to the technology changes, I wanted to use the upgrade as an excuse to clean-up some messy stuff we had in place. An example of which being the amount of custom transformation we were doing in Java code.

Our integration implementation makes heavy use of the Canonical Data Model pattern.  To shortly sum it up , we accept data in a variety of formats (XML, CSV or proprietary) and map them to an XML schema and/or Java object model.  Beyond the standard transport transformations supplied by Mule, we needed to implement a zoo of custom transformers to move to the canonical format.  I was looking for a way to mitigate this complexity overhead in some sort of framework.

I had read this article on InfoQ about Smooks around when thinking about the above and it seemed like a good fit, especially since there is a Mule module for it.  To make a long story short, we were able to upgrade to Mule 2.x and, using Smooks, not have to implement any model specific Mule transformers.  

Smooks works by streaming data in, transforming it and streaming it out.  "Cartridges" supply various transformation capabilities and exist for common data formats like XML, JSON and CSV.  The streaming model means that the transformations themselves don't require the entire documents to be loaded in memory.  This allows for large documents to be transformed without requiring the associated memory footprint.

The transformations can  be accomplished via XML configuration assuming the data formats being used have an associated cartridge.  This is also the case if the data is in a format you can easily move to a different format.  For instance, we have Nagios 2.x instances that use a semi-colon delimited status.log to write alert data.  A simple Groovy script allowed me to replace the semi-colons with commas.  I was then able to use the CSV cartridge to convert the data to XML.

The above Nagios instances are being upgraded to Nagios 3.x.  In Nagios 3.x, the status.log format is different.  Instead of being semi-colon delimited, it is in a proprietary format that sort of looks like JSON.  Here's an example:


servicestatus {
host_name=liro_url_laces0
service_description=liro_https://acmesoft.com/VI/Pages/General/TestConn.aspx
modified_attributes=0
check_command=check_https!/VI/
check_period=24x7
notification_period=24x7
check_interval=15.000000
retry_interval=2.000000
event_handler=
has_been_checked=1
..
}
There obviously isn't a Smooks cartridge that supports this format.  One solution might be to try to convert the above format to JSON.  This will probably work but likely be error-prone  (and annoying to implement.)  An alternative is to implement an XMLReader to parse the above file and spit out an XML Document.  

Smooks uses implementations of XMLReader to parse arbitrary file formats as XML.  It then operate on the SAX stream or DOM as dictated by a configuration file.  The following illustrates an implementation of the parse method of XMLReader that will parse the status.log format above:



public void parse(InputSource inputSource) throws IOException, SAXException {
if (contentHandler == null) {
throw new IllegalStateException("'contentHandler' not set. Cannot parse Email stream.");
}

String currentBlock = null;

contentHandler.startDocument();
contentHandler.startElement(XMLConstants.NULL_NS_URI, "statusLog", "", EMPTY_ATTRIBS);

for (String line : getString(inputSource).split("\n")) {

if (line.startsWith("#"))
continue;

if (line.contains("servicestatus")) {
String block = StringUtils.deleteWhitespace(line.split("\\{")[0]);
contentHandler.startElement(XMLConstants.NULL_NS_URI, block, "", EMPTY_ATTRIBS);
currentBlock = block;
}

if (currentBlock != null) {
if (line.contains("=")) {
String[] fields = line.split("=", 2);
String fieldName = StringEscapeUtils.escapeXml(StringUtils.deleteWhitespace(fields[0].replace("=", "")));

contentHandler.startElement(XMLConstants.NULL_NS_URI, fieldName, "", EMPTY_ATTRIBS);
if (fields.length > 1) {
String content = StringEscapeUtils.escapeXml(fields[1]);

contentHandler.characters(content.toCharArray(), 0, content.length());
} else {
contentHandler.characters(" ".toCharArray(), 0, 1);
}
contentHandler.endElement(XMLConstants.NULL_NS_URI, fieldName, "");
}

if (line.contains("}")) {
contentHandler.endElement(XMLConstants.NULL_NS_URI, currentBlock, "");
currentBlock = null;
}
}

}

contentHandler.endElement(XMLConstants.NULL_NS_URI, "statusLog", "");
contentHandler.endDocument();
}

We can plug the reader into the Smooks XML config :


<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
xmlns:csv="http://www.milyn.org/xsd/smooks/csv-1.1.xsd"
xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd"
>

<params>
<param name="stream.filter.type">SAX</param>
<param name="default.serialization.on">false</param>
</params>

<reader class="net.opsource.osb.reader.NagiosReader"/>

<resource-config selector="servicestatus">
<resource>org.milyn.delivery.DomModelCreator</resource>
</resource-config>

<ftl:freemarker applyOnElement="statusLog">
<ftl:template><!--
<ApplicationResponseTimes>
<?TEMPLATE-SPLIT-PI?>
</ApplicationResponseTimes>
-->
</ftl:template>
</ftl:freemarker>

<ftl:freemarker applyOnElement="servicestatus">
<ftl:template>smooks/monitoring/application_response_time/metric.ftl</ftl:template>
</ftl:freemarker>

</smooks-resource-list>



Now we plug it into Mule using the Smooks module and we're ready to go.


<smooks:transformer name="nagiosStatusLineToXML"
configFile="smooks/monitoring/application_response_time/smooks-config.xml"
resultType="STRING"/>


I'm pretty excited about this because I'm no longer writing a dedicated transformer for each domain model I'm mapping data to. I just need to implement XMLReaders when I come across a data format not already supported by a Smooks cartridge.

Thursday, January 15, 2009

OpenMQ, Second Thoughts

 OpenMQ was fairly painless to get going with Mule.  I opted to set it up in our staging environment as a conventional cluster with 2 nodes. In this scenario, clients can maintain a list of brokers to connect to in the event one of them fails.  Loadbalancing between brokers might also be supported, but I haven't gone too far down that rabbit hole yet.  

Nor have I gone down the rabbit hole of HA clusters, which support failover of message data between brokers but require a shared database.  Amongst other things, we're using JMS to distribute monitoring alerts.  To do HA in production in a sensible manner, we'd need to back OpenMQ against our production MySQL cluster.  Since we're sending monitoring data about the same production MySQL cluster over JMS,  if the MySQL cluster failed we'd never hear about it.  We're not (currently) using JMS for any sort of financial or mission-critical data so losing a few messages in the event of a failover isn't too big of a deal for us as long as its a rare occurrence.  

Getting clients to connect to OpenMQ was a little more painful. It manages all of its "objects" (connection factories, queues, topics, etc) in JNDI.  We don't really use any sort of distributed JNDI infrastructure, so I started off using the filesystem JNDI context supplied with OpenMQ. In this scenario, all your OpenMQ objects are stored in a hidden file in a directory.  This works fine in development or testing situations when your clients and broker all have access to the directory.  Its obviously not an option for production, unless you do something painful like make tarballs of the JNDI filesystem directory and scp them or around or export it over NFS.    

According to the documentation, the "right" way seems to be by using an LDAP directory context to store the JNDI data (someone please correct me if I'm wrong about this.)  In this case, you store your OpenMQ objects to LDAP.  Each client then loads the appropriate connection factory, queus, etc from LDAP.  This is nice in the sense that configuration data for the connections (broker lists, etc) are maintained outside of the clients.  Presumably this allows you to add brokers to a cluster,  etc w/o having to restart your JMS clients.  

Despite the bit of complexity, this again was pretty straightforward. I just needed an LDAP directory to store the JNDI data in.  It (briefly) occurred to me to use our Active Directory deployment.  My assumption was, however, that this would involve modifying Active Directory's schema which I've never done before and heard nightmare stories about(it would also involve making changes to the production AD deployment - which is treated akin to a live hand grenade in the company.)

I ultimately opted to use OpenLDAP.  This was painless.  The only thing I had to do was include the supplied java.schema in the the slapd.conf and restart the service.  A short while later I was able to get Mule and JMeter sending messages through it.  The OpenMQ command line stuff worked great while doing some preliminary load testing.  The queue metrics in particular were really nice - it lets you watch queue statistics the same way you'd watch memory statistics with vmstat or disk statistics with iostat.  I am pretty impressed so far...

Wednesday, January 14, 2009

OpenMQ, First Thoughts

For a variety of reasons I've been evaluating message brokers other then ActiveMQ.  I installed OpenMQ after hearing anecdotal evidence it was a good alternative.  Before switching to doing development full-time, I spent a fair amount of time in the trenches admining Solaris and Linux boxes.  As such, I'm a little bit too familiar with Sun's weird installers for things - and OpenMQ was no exception.  I was confronted with the first ncurses GUI I've seen since I stopped using Mutt to read my email - a tarball or RPM would have been sufficient. 

Despite this, the installation went smoothly enough and I had the broker up and going in about 15 minutes.  I had two brokers up in a cluster 45 minutes later.  All through the command line. Which was extremely impressive.  I'm going to spend some time today getting Mule and JMeter going with it and start passing some messages through.  I'll follow up with how that goes...

Wednesday, December 3, 2008

Mule in Action

I've been debating starting one of these for a while now, but now that I'm wrapping up a book its become inevitable.  I'll be using this space to discuss the book (Mule in Action) I've been writing with David Dossot along with other development sort of stuff.

Tuesday, November 11, 2008