Tuesday, June 9, 2009

A Spring Loaded Pipeline Processor for Orbeon

As many Java developers and architects know, the Spring framework is a very effective tool that facilitates Java development and makes it more enjoyable. Being myself an aficionado, I was looking for a way to use Spring configured services from within my Orbeon XForms application. My idea and requirement was essentially to be able to access these services from within Orbeon pipeline processors (XPL). While the final and ultimate solution is still to come, I thought I should present the basic idea and preliminary implementation here and possibly get some feedback and suggestions.

My solution is quite generic and makes use of Java reflection (just as Spring does) to expose Spring configured POJOs to pipeline programs. Essentially, the end result as it is used inside an XPL program looks like this:
<p:processor name="wf:javaServices">
<p:input name="instance" href="#input-doc"/>
<p:input name="config">
<config service="userServices"
javaType="com.opnworks.workforse.ops.processor.UserServicesBroker"
method="getUserDetails"
xsl:version="2.0">
<argument type="xsd:string">arg1</argument>
<argument type="xsd:string">arg2</argument>
<argument ref="instance"/>
</config>
</p:input>
<p:output name="data" id="result" />
</p:processor>


In the above XPL element, we use a custom processor (wf:javaServices) that was registered as an Orbeon processor in the custom-processor.xml file. This processor requires a "config" input which identifies the Java bean defined in the Spring application context. This is done by using the @service attribute on the <config> element. This attribute corresponds to the name of the Spring-defined baen in hte applicaiton context. We must also specify a javaType attribute specifying the class (or interface) of the bean or service that we want to access. Finally, the @method attribute contains, as you have already guessed, the name of the method we want to invoke. Finally, the <config> element contain 0 or more <argument> children corresponding to the method arguments. These can be scalar values in which case we need to specify a type in the @type attribute. Alternatively, an argument can contain a reference (using the @ref attribute) to one of the processor inputs (an XML document) that will be passed to the service through the method. Naturally, the order of the arguments is important since the information specified in the <config> element will be used to reflectively lookup the method on the Java class specified in the @service attribute.

My custom processor extends org.orbeon.oxf.processor.SimpleProcessor and basically defines the following method. Note that this processor needs to be able to access a Spring application context (the ctx variable in code below) and this context is typically loaded at the time the custom processor class is loaded.

public void generateData(PipelineContext context,
ContentHandler contentHandler) throws SAXException {

Element configRoot = readInputAsDOM4J(context, "config").getRootElement();

String serviceName = configRoot.attributeValue("service");
String serviceType = configRoot.attributeValue("javaType");
Class clazz = null;
try {
clazz = getClass().getClassLoader().loadClass(serviceType);
} catch (ClassNotFoundException e1) {
throw new OXFException(e1);
}

Object service = ctx.getBean(serviceName, clazz);

String methodName = configRoot.attributeValue("method");
List<Element> argumentNodes = configRoot.selectNodes("//argument");
Object[] arguments = new Object[argumentNodes.size()];
Class[] parameterTypes = new Class[argumentNodes.size()];

for (int i = 0; i < argumentNodes.size(); i++) {

String type = argumentNodes.get(i).attributeValue("type");
if (type == null) {
Document doc = readInputAsDOM4J(context, argumentNodes.get(i).attributeValue("ref"));
arguments[i] = doc;
parameterTypes[i] = Document.class;
}
else {
arguments[i] = getAttributeValue(argumentNodes.get(i));
parameterTypes[i] = arguments[i].getClass();
}
}
try {
Method method = service.getClass().getMethod(methodName, parameterTypes);
Document doc = (Document) method.invoke(service, arguments);
LocationSAXWriter saxWriter = new LocationSAXWriter();
saxWriter.setContentHandler(contentHandler);
saxWriter.write(doc);
} catch (Exception e) {
throw new OXFException(e);
}
}
This approach works like a charm and allows me to extend ad infinitum the scope of the java services I want to expose to XPL programs without having to write and register additional processors.

What do you think?

2 comments:

  1. Very clever! Any hints on how to set the "ctx" ?

    ReplyDelete
  2. I used a very simple and I would say unsophisticated approach that solved my problem.

    In my SimpleProcessor subclass, I just set a static variable to contain the application context at class load time:

    private static ApplicationContext ctx;

    static { ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    }

    ReplyDelete