Saxon from Java — parameters and Java extensions¶
The Java APIs page introduced Saxon as the way to run modern
XSLT 3.0 / XPath 3.1 / XQuery 3.1 on the JVM. This page
goes one level deeper into the part that turns Saxon from "a transformer you call"
into "a language you embed and extend": feeding parameters in from Java, and
calling your own Java code back out from inside a stylesheet. Both run on the
free, open-source Saxon-HE; everything below was compiled and run against
net.sf.saxon:Saxon-HE:12.5.
The idiomatic API is s9api (net.sf.saxon.s9api.*) — a small, typed object
model that wraps Saxon's internals. Four object families do almost everything:
flowchart LR
P["Processor"] -->|"newXsltCompiler()"| C["XsltCompiler"]
C -->|"compile()"| E["XsltExecutable"]
E -->|"load30()"| T["Xslt30Transformer"]
P -.->|"newXPathCompiler()<br/>newXQueryCompiler()"| Q["XPath / XQuery"]
P -.->|"registerExtensionFunction()"| X["your Java"]
A Processor is the shared, thread-safe factory you create once. Compilers turn
source into a reusable …Executable; loading an executable gives a per-run
transformer or selector. Values that cross the boundary are XdmValue and its kin
(XdmItem, XdmAtomicValue, XdmNode), and names are always QName — the
namespace-aware name object, never a raw string.
Passing parameters in¶
XSLT has two different things people both call "parameters", and Saxon keeps them
on separate methods. Both take a Map<QName, XdmValue>.
Processor proc = new Processor(false); // (1)!
XsltExecutable exec = proc.newXsltCompiler()
.compile(new StreamSource(new File("report.xsl")));
Xslt30Transformer t = exec.load30(); // a fresh per-run transformer
// 1. global xsl:param declared at the top of the stylesheet
t.setStylesheetParameters(Map.of(
new QName("who"), new XdmAtomicValue("Saxon"),
new QName("currency"), new XdmAtomicValue("EUR"))); // (2)!
// 2. params to the *initial named template* — the entry point you call below
t.setInitialTemplateParameters(
Map.of(new QName("punct"), new XdmAtomicValue("!")),
false); // (3)!
new Processor(false)—falsemeans "not schema-aware", i.e. Saxon-HE. There is oneProcessorper application; compilers and transformers are spun off it.- Global stylesheet parameters feed the top-level
<xsl:param>elements — the stylesheet's configuration knobs. TheQNamemust match the param's name (and namespace, if any); the document's prefix is irrelevant, only the URI is. - Initial-template parameters are different: they are arguments to the
specific template you invoke, not document-wide. The boolean is the
tunnel flag —
truemakes them tunnel parameters that flow throughapply-templatesto deep descendants without being re-declared at each level.
Choosing the entry point¶
Xslt30Transformer does not have a single "go" method — XSLT 3.0 lets you start a
transform three ways, and each is a method:
Serializer out = proc.newSerializer(new File("out.xml"));
t.applyTemplates(new StreamSource(new File("data.xml")), out); // (1)!
t.callTemplate(new QName("main"), out); // (2)!
XdmValue r = t.callFunction(new QName(NS, "format-total"), // (3)!
new XdmValue[]{ new XdmAtomicValue(100) });
- Push a source document through the template rules — the classic
"match templates against an input tree" model. This is the one that consumes
setStylesheetParameters. - Call a named template with no input document — handy for generating output
from parameters alone (a report header, a config file). This consumes the
setInitialTemplateParametersset above. - Call a named
xsl:functiondirectly and get itsXdmValueback — using a stylesheet as a library of pure functions, no serialization needed.
Extension functions — calling Java from XSLT¶
The reverse direction is the interesting one: making your own Java reachable from inside a stylesheet or XPath expression — a database lookup, a crypto routine, a date formatter the language lacks. Saxon offers two levels.
The simple, portable way: ExtensionFunction¶
net.sf.saxon.s9api.ExtensionFunction is a four-method interface (one of them a
default), built entirely from s9api types. You implement it, register it on the
Processor, and it is callable under whatever namespace you gave its name —
and it works in Saxon-HE.
- The function's name is a
QName— a local name in a namespace you own. Mint a URI for your extensions (http://acme.example/fnhere); never put them in a W3C or vendor namespace. getResultTypeis adefaultmethod (you may omit it and Saxon assumesitem()*), but declaring it lets Saxon type-check calls.SequenceTypepairs anItemTypewith anOccurrenceIndicator(ONE,ZERO_OR_ONE,ZERO_OR_MORE,ONE_OR_MORE) — the Java spelling of XPath'sxs:string,xs:string?,xs:string*.- Argument types, in order — here a single required string. Saxon uses these to bind and convert the call's arguments before your code runs.
- The body. Arguments arrive as
XdmValue[]; you return anXdmValue. Everything is in Saxon's data model, sogetStringValue(),XdmAtomicValue, and friends are how you cross in and out of plain Java. - One call wires it into the
Processor; every stylesheet and XPath compiled from that processor can now see it.
Bind the namespace in the stylesheet and call it like any other function:
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ex="http://acme.example/fn" <!-- (1)! -->
exclude-result-prefixes="ex" expand-text="yes">
<xsl:param name="who" as="xs:string" select="'world'"
xmlns:xs="http://www.w3.org/2001/XMLSchema"/>
<xsl:template name="main">
<xsl:param name="punct" as="xs:string" select="'.'"
xmlns:xs="http://www.w3.org/2001/XMLSchema"/>
<out>{ex:shout('hello ' || $who)}{$punct}</out> <!-- (2)! -->
</xsl:template>
</xsl:stylesheet>
ex:is bound to the same URI yourgetName()returned. That URI is the only link between the stylesheet and the Java class — the prefix is cosmetic.ex:shout(...)calls into Java;$whoand$punctare the two parameters set from Java above. With the parameters from the previous section (who="Saxon",punct="!"),callTemplate("main")produces:
The full-control way: ExtensionFunctionDefinition¶
When you need variable arity, access to the evaluation context (the current
node, the static base URI), or lazy argument evaluation, drop to
net.sf.saxon.lib.ExtensionFunctionDefinition — a lower-level abstract class that
works in terms of Saxon's internal types rather than s9api:
public class LookupFn extends ExtensionFunctionDefinition {
public StructuredQName getFunctionQName() { // internal QName type
return new StructuredQName("ex", "http://acme.example/fn", "lookup");
}
public int getMinimumNumberOfArguments() { return 1; } // (1)!
public int getMaximumNumberOfArguments() { return 2; }
public net.sf.saxon.value.SequenceType[] getArgumentTypes() { /* … */ }
public net.sf.saxon.value.SequenceType getResultType(
net.sf.saxon.value.SequenceType[] suppliedArgs) { /* … */ }
public ExtensionFunctionCall makeCallExpression() { // (2)!
return new ExtensionFunctionCall() {
public Sequence call(XPathContext ctx, Sequence[] args) { /* … */ }
};
}
}
proc.registerExtensionFunction(new LookupFn()); // (3)!
- Variable arity —
ex:lookup($key)orex:lookup($key, $default)from the one definition. The simpleExtensionFunctioninterface above is fixed-arity. makeCallExpression()returns the object that actually runs, and itscallreceives theXPathContext— so this is the level at which you can read the context node or integrate with Saxon's lazy evaluation.- The same
registerExtensionFunctionmethod takes either kind — there is an overload forExtensionFunctionand one forExtensionFunctionDefinition.
Reflexive java: functions are not the HE path
Saxon also supports reflexive extension functions — binding a namespace
straight to a class (xmlns:m="java:java.lang.Math") and calling
m:sqrt(2.0) with no registration. It is convenient, but it is gated behind
Saxon-PE/EE (and is a code-execution surface you may not want stylesheets
to have). The integrated functions above are the portable, Saxon-HE-safe,
explicitly-allow-listed way — you decide exactly which Java each processor can
reach. This is the same HE/PE/EE line drawn on the
Java APIs page.
XPath and XQuery with external variables¶
The same value-passing idea applies when you run a bare XPath or XQuery from Java: declare the variable on the compiler, bind it on the selector.
XPathCompiler xpc = proc.newXPathCompiler();
xpc.declareNamespace("i", "urn:example:invoice"); // (1)!
xpc.declareVariable(new QName("threshold"));
XPathSelector sel = xpc.compile("//i:total[. > $threshold]").load();
sel.setContextItem(proc.newDocumentBuilder().build(new File("invoice.xml")));
sel.setVariable(new QName("threshold"), new XdmAtomicValue(50)); // (2)!
XdmValue hits = sel.evaluate();
- Saxon's XPath takes namespaces as a plain
declareNamespace(prefix, uri)— much nicer than JAXP'sNamespaceContext(shown on the Java page). - The variable is declared at compile time, bound at run time — so the same
compiled
XPathExecutablecan be re-run with different values. XQuery is the twin:XQueryEvaluator.setExternalVariable(QName, XdmValue)feeds adeclare variable $x external;.
Things to note¶
- One
Processorper application; compile once into an…Executableand reuse it — the compile is the costly step, not the run. - Two kinds of XSLT parameter: global
setStylesheetParameters(stylesheet config) versus per-invocationsetInitialTemplateParameters(with a tunnel flag). They are not interchangeable. - Three entry points —
applyTemplates(push a document),callTemplate(named entry, no input),callFunction(use the stylesheet as a function library). - Integrated extension functions let stylesheets call your Java and run on
free Saxon-HE; reach for
ExtensionFunctionDefinitiononly when you need variable arity or the evaluation context. Avoid reflexivejava:binding unless you are on PE/EE and accept the security surface. - Values crossing the boundary are always
XdmValue/QName— Saxon's typed, namespace-aware model, never raw strings.
Compare with the broader Java API survey, or the .NET, Python and Rust pages — the latter two reach this same Saxon engine through Saxon-C.