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.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.
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);
}
}
What do you think?
Very clever! Any hints on how to set the "ctx" ?
ReplyDeleteI used a very simple and I would say unsophisticated approach that solved my problem.
ReplyDeleteIn 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");
}