Friday, July 31, 2009

eXist extension functions in Java

Investigating into opportunities for an implementation of EXPath Packaging for eXist, I am looking at the way to implement extension functions with Java. I detail here the key points I found to write Java extensions for eXist using NetBeans. But those info should be generic enough to be used with any Java IDE.

If you want to provide an extension function written in Java within eXist, you actually have to provide a full module in Java. You cannot provide the module part in XQuery and part in Java (though you can provide an XQuery module that imports a private module written in Java.)

The module is represented by a class that extends the eXist abstract class AbstractInternalModule. Along some information like the namespace URI of the module, the default prefix to use, and a description, you have to provide a list of extension functions through the base class constructor:

public class SimpleModule extends AbstractInternalModule
{
    public SimpleModule() {
        super(FUNCTIONS);
    }
    public String getNamespaceURI() {
        return NAMESPACE_URI;
    }
    public String getDefaultPrefix() {
        return PREFIX;
    }
    public String getDescription() {
        return "A simple example module";
    }

    static final String PREFIX = "ext";
    static final String NAMESPACE_URI = "http://www.example.org/project/ext";

    private final static FunctionDef[] FUNCTIONS = {
        new FunctionDef(SimpleFunction.SIGNATURE, SimpleFunction.class)
    };
}

This module contains one single function, implemented by the class SimpleFunction. An extension function is a Java class that extends the eXist abstract class BasicFunction. It has to provide some information to the base class constructor (its qualified name, a documentation string, its parameter list and its return type.) The logic of the function itself is located in the eval function:

// implements the function "ext:hello($who as xs:string) as element()".
public class SimpleFunction extends BasicFunction
{
    public SimpleFunction(XQueryContext ctxt) {
        super(ctxt, SIGNATURE);
    }

    /** return an element <hello> with the value of the string param as content */
    public Sequence eval(Sequence[] args, Sequence unused) throws XPathException {
        String type = Integer.toString(args[0].getItemType());
        MemTreeBuilder builder = context.getDocumentBuilder();
        builder.startDocument();
        builder.startElement(new QName("hello", null, null), null);
        builder.characters(args[0].getStringValue());
        builder.endElement();
        builder.endDocument();
        return (Sequence) builder.getDocument().getDocumentElement();
    }

    private static final FunctionParameterSequenceType PARAM_WHO =
            new FunctionParameterSequenceType("who", Type.STRING, Cardinality.EXACTLY_ONE, "Who?");
    private static final FunctionReturnSequenceType RETURN_TYPE =
            new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE, "Hello element");

    static final FunctionSignature SIGNATURE = new FunctionSignature(
        new QName("hello", SimpleModule.NAMESPACE_URI, SimpleModule.PREFIX),
        "Return greetings."
        + " This method returns an element 'hello' with the name passed as param.",
        new SequenceType[] { PARAM_WHO },
        RETURN_TYPE
      );
}

In order to compile those files with NetBeans, you have to add two JAR files to the build path of the project you used for those files. Right-click on the project and choose Properties, then Libraries and the button Add JAR/Folder. You will find the JARs in the eXist install directory: $EXIST_HOME/exist.jar and $EXIST_HOME/lib/core/xmldb.jar. Then you compile your project to get the JAR file with your both class (the module and its single function) compiled. The best choice for the project's type is a library of Java classes.

Now you have the JAR file containing the Java implementation of your module and its function. The next step is to plug them within an eXist instance. Copy the JAR file to $EXIST_HOME/lib/extensions/ and configure it in $EXIST_HOME/conf.xml. The configuration only requires to add a new module element to the builtin-modules element:

<module class="org.example.project.SimpleModule"
        uri="http://www.example.org/project/ext"/>

Just restart your eXist instance, and try the following query in the admin console:

import module namespace ext="http://www.example.org/project/ext";
ext:hello('you')

This will call the method eval on a SimpleFunction object, discovered through a SimpleModule object configured in conf.xml. Soon, you should be able to package the JAR file to an EXPath package and install it within eXist through a graphical console...

Labels: , ,