net.sf.saxon.s9api.XQueryEvaluator Maven / Gradle / Ivy
Show all versions of Saxon-HE Show documentation
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2023 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package net.sf.saxon.s9api;
import net.sf.saxon.Configuration;
import net.sf.saxon.Controller;
import net.sf.saxon.event.*;
import net.sf.saxon.expr.instruct.Executable;
import net.sf.saxon.expr.instruct.GlobalContextRequirement;
import net.sf.saxon.expr.instruct.UserFunction;
import net.sf.saxon.expr.parser.Loc;
import net.sf.saxon.expr.parser.RoleDiagnostic;
import net.sf.saxon.lib.*;
import net.sf.saxon.om.*;
import net.sf.saxon.query.DynamicQueryContext;
import net.sf.saxon.query.XQueryExpression;
import net.sf.saxon.s9api.streams.XdmStream;
import net.sf.saxon.serialize.SerializationProperties;
import net.sf.saxon.trans.UncheckedXPathException;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.transpile.CSharpModifiers;
import net.sf.saxon.tree.tiny.TinyBuilder;
import net.sf.saxon.type.TypeHierarchy;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMSource;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Supplier;
/**
* An XQueryEvaluator
represents a compiled and loaded query ready for execution.
* The XQueryEvaluator
holds details of the dynamic evaluation context for the query.
* An XQueryEvaluator
must not be used concurrently in multiple threads.
* It is safe, however, to reuse the object within a single thread to run the same
* query several times. Running the query does not change the context
* that has been established.
* An XQueryEvaluator
is always constructed by running the Load
* method of an {@link net.sf.saxon.s9api.XQueryExecutable}.
* An XQueryEvaluator
is itself a Iterable
. This makes it possible to
* evaluate the results in a for-each expression.
* An XQueryEvaluator
is itself a Destination
. This means it is possible to use
* one XQueryEvaluator
as the destination to receive the results of another transformation,
* this providing a simple way for transformations to be chained into a pipeline. When the query is executed
* this way, {@link #setDestination(Destination)} must be called to provide a destination for the result of this query.
* As a {@code Destination}, an {@code XQueryEvaluator} performs sequence normalization as defined
* in the Serialization specification, including inserting item separators if required. The input to the
* query (the global context item) will therefore always be a single document node. This will always be built
* as a tree in memory, it will never be streamed.
*/
@CSharpModifiers(code = {"internal"})
public class XQueryEvaluator extends AbstractDestination implements Iterable {
private final Processor processor;
private final XQueryExpression expression;
private final DynamicQueryContext context;
private Controller controller; // used only when making direct calls to global functions
private Destination destination;
private Set updatedDocuments;
/*@Nullable*/ private Builder sourceTreeBuilder;
/**
* Protected constructor
*
* @param processor the Saxon processor
* @param expression the XQuery expression
*/
protected XQueryEvaluator(Processor processor, XQueryExpression expression) {
this.processor = processor;
this.expression = expression;
this.context = new DynamicQueryContext(expression.getConfiguration());
}
/**
* Set the schema validation mode for the transformation. This indicates how source documents
* loaded specifically for this transformation will be handled. This applies to the
* principal source document if supplied as a SAXSource or StreamSource, and to all
* documents loaded during the transformation using the doc()
, document()
,
* or collection()
functions.
*
* @param mode the validation mode. Passing null causes no change to the existing value.
* Passing Validation.DEFAULT
resets to the initial value, which determines
* the validation requirements from the Saxon Configuration.
*/
public void setSchemaValidationMode(ValidationMode mode) {
if (mode != null) {
context.setSchemaValidationMode(mode.getNumber());
}
}
/**
* Get the schema validation mode for the transformation. This indicates how source documents
* loaded specifically for this transformation will be handled. This applies to the
* principal source document if supplied as a SAXSource or StreamSource, and to all
* documents loaded during the transformation using the doc()
, document()
,
* or collection()
functions.
*
* @return the validation mode.
*/
public ValidationMode getSchemaValidationMode() {
return ValidationMode.get(context.getSchemaValidationMode());
}
/**
* Set the source document for the query.
* If the source is an instance of {@link net.sf.saxon.om.NodeInfo}, the supplied node is used
* directly as the context node of the query.
* If the source is an instance of {@link javax.xml.transform.dom.DOMSource}, the DOM node identified
* by the DOMSource is wrapped as a Saxon node, and this is then used as the context item
* In all other cases a new Saxon tree is built, by calling
* {@link net.sf.saxon.s9api.DocumentBuilder#build(javax.xml.transform.Source)}, and the document
* node of this tree is then used as the context item for the query.
*
* @param source the source document to act as the initial context item for the query.
* @throws SaxonApiException if an error is detected
*/
public void setSource(Source source) throws SaxonApiException {
if (source instanceof NodeInfo) {
setContextItem(new XdmNode((NodeInfo) source));
} else if (source instanceof DOMSource) {
setContextItem(processor.newDocumentBuilder().wrap(source));
} else {
setContextItem(processor.newDocumentBuilder().build(source));
}
}
/**
* Set the initial context item for the query
*
* @param item the initial context item, or null if there is to be no initial context item
* @throws SaxonApiException if the query declares the context item and does not define
* it to be external
*/
public void setContextItem(XdmItem item) throws SaxonApiException {
if (item != null) {
GlobalContextRequirement gcr = expression.getExecutable().getGlobalContextRequirement();
if (gcr != null && !gcr.isExternal()) {
throw new SaxonApiException("The context item for the query is not defined as external");
}
context.setContextItem(item.getUnderlyingValue());
}
}
/**
* Get the initial context item for the query, if one has been set
*
* @return the initial context item, or null if none has been set. This will not necessarily
* be the same object as was supplied, but it will be an XdmItem object that represents
* the same underlying node or atomic value.
*/
public XdmItem getContextItem() {
Item item = context.getContextItem();
if (item == null) {
return null;
}
return (XdmItem) XdmValue.wrap(item);
}
/**
* Set the value of external variable defined in the query
*
* @param name the name of the external variable, as a QName
* @param value the value of the external variable, or null to clear a previously set value
* @throws SaxonApiUncheckedException if the value is evaluated lazily, and evaluation fails
*/
public void setExternalVariable(QName name, XdmValue value) {
try {
context.setParameter(name.getStructuredQName(),
value == null ? null : ((Sequence) value.getUnderlyingValue()).materialize());
} catch (UncheckedXPathException e) {
throw new SaxonApiUncheckedException(e);
}
}
/**
* Get the value that has been set for an external variable
*
* @param name the name of the external variable whose value is required
* @return the value that has been set for the external variable, or null if no value has been set
*/
public XdmValue getExternalVariable(QName name) {
GroundedValue oval = context.getParameter(name.getStructuredQName());
if (oval == null) {
return null;
}
return XdmValue.wrap(oval);
}
/**
* Set an object that will be used to resolve URIs used in
* fn:doc() and related functions.
*
* @param resolver An object that implements the URIResolver interface, or
* null.
* @deprecated since 11.1 - use a ResourceResolver
instead
*/
@Deprecated
public void setURIResolver(URIResolver resolver) {
context.setResourceResolver(new ResourceResolverWrappingURIResolver(resolver));
}
/**
* Get the URI resolver.
*
* @return the user-supplied URI resolver if there is one, or the
* system-defined one otherwise
* @deprecated since 11.1 - use a ResourceResolver
instead
*/
@Deprecated
public URIResolver getURIResolver() {
ResourceResolver rr = context.getResourceResolver();
if (rr instanceof ResourceResolverWrappingURIResolver) {
return ((ResourceResolverWrappingURIResolver) rr).getWrappedURIResolver();
} else {
return null;
}
}
/**
* Set the ResourceResolver to be used during query evaluation.
* @param resolver the ResourceResolver to be used during query evaluation.
*/
public void setResourceResolver(ResourceResolver resolver) {
context.setResourceResolver(resolver);
}
/**
* Get the ResourceResolver to be used during query evaluation.
*
* @return the ResourceResolver used during query evaluation. Returns null if no user-supplied
* ResourceResolver has been set.
*/
public ResourceResolver getResourceResolver() {
return context.getResourceResolver();
}
/**
* Set an object that will be used to resolve URIs used in
* fn:unparsed-text()
and related functions.
*
* @param resolver An object that implements the UnparsedTextURIResolver interface, or
* null.
* @since 11
*/
public void setUnparsedTextResolver(UnparsedTextURIResolver resolver) {
context.setUnparsedTextURIResolver(resolver);
}
/**
* Get the URI resolver used for fn:unparsed-text()
and related functions.
*
* @return the user-supplied URI resolver if there is one, or the
* system-defined one otherwise
* @since 11
*/
public UnparsedTextURIResolver getUnparsedTextURIResolver() {
return context.getUnparsedTextURIResolver();
}
/**
* Set the error listener. The error listener receives reports of all run-time
* errors and can decide how to report them.
*
* @param listener the ErrorListener to be used
*/
public void setErrorListener(ErrorListener listener) {
context.setErrorListener(listener);
}
/**
* Get the error listener.
*
* @return the ErrorListener in use
*/
@SuppressWarnings("deprecation")
public ErrorListener getErrorListener() {
return context.getErrorListener();
}
/**
* Supply a callback which will be notified of all dynamic errors and warnings
* encountered during this query evaluation.
* Calling this method overwrites the effect of any previous call on {@link #setErrorListener(ErrorListener)}
* or {@code setErrorList}.
* If no error reporter is supplied by the caller, error information will be written to the standard error stream.
*
* @param reporter a callback function which will be notified of all Static errors and warnings
* encountered during a compilation episode.
* @since 11.2
*/
public void setErrorReporter(ErrorReporter reporter) {
context.setErrorReporter(reporter);
}
/**
* Get the recipient of error information previously registered using {@link #setErrorReporter(ErrorReporter)}.
*
* @return the recipient previously registered explicitly using {@link #setErrorReporter(ErrorReporter)},
* or implicitly using {@link #setErrorListener(ErrorListener)}.
* If no error reporter has been registered, the result may be null, or may return
* a system supplied error reporter.
* @since 11.2
*/
public ErrorReporter getErrorReporter() {
return context.getErrorReporter();
}
/**
* Set a TraceListener which will receive messages relating to the evaluation of all expressions.
* This option has no effect unless the query was compiled to enable tracing.
*
* @param listener the TraceListener to use
*/
public void setTraceListener(TraceListener listener) {
context.setTraceListener(listener);
}
/**
* Get the registered TraceListener, if any
*
* @return listener the TraceListener in use, or null if none has been set
*/
public TraceListener getTraceListener() {
return context.getTraceListener();
}
/**
* Set the destination for output from the fn:trace() function.
* By default, the destination is System.err. If a TraceListener is in use,
* this is ignored, and the trace() output is sent to the TraceListener.
*
* @param stream the PrintStream to which trace output will be sent. If set to
* null, trace output is suppressed entirely. It is the caller's responsibility
* to close the stream after use.
* @since 9.1. Changed in 9.6 to use a Logger
*/
public void setTraceFunctionDestination(Logger stream) {
context.setTraceFunctionDestination(stream);
}
/**
* Get the destination for output from the fn:trace() function.
*
* @return the PrintStream to which trace output will be sent. If no explicitly
* destination has been set, returns System.err. If the destination has been set
* to null to suppress trace output, returns null.
* @since 9.1. Changed in 9.6 to use a Logger
*/
public Logger getTraceFunctionDestination() {
return context.getTraceFunctionDestination();
}
/**
* Set the destination to be used for the query results
*
* @param destination the destination to which the results of the query will be sent
*/
public void setDestination(Destination destination) {
this.destination = destination;
}
/**
* Perform the query.
* - In the case of a non-updating query, the results are sent to the
* registered Destination.
* - In the case of an updating query, all updated documents will be available after query
* execution as the result of the {@link #getUpdatedDocuments} method.
*
*
* @throws net.sf.saxon.s9api.SaxonApiException
* if any dynamic error occurs during the query
* @throws IllegalStateException if this is a non-updating query and no Destination has been
* supplied for the query results
*/
public void run() throws SaxonApiException {
try {
if (expression.isUpdateQuery()) {
Set docs = expression.runUpdate(context);
updatedDocuments = new HashSet<>();
for (MutableNodeInfo doc : docs) {
updatedDocuments.add(XdmItem.wrapItem(doc));
}
} else {
if (destination == null) {
throw new IllegalStateException("No destination supplied");
}
run(destination);
// Result receiver;
// if (destination instanceof Serializer) {
// //receiver = ((Serializer) destination).getResult();
// //context.set
// receiver = ((Serializer) destination).getReceiver(expression.getExecutable());
// } else {
// receiver = destination.getReceiver(expression.getConfiguration());
// }
// expression.run(context, receiver, null);
// destination.close();
}
} catch (TransformerException e) {
throw new SaxonApiException(e);
} catch (UncheckedXPathException e) {
throw new SaxonApiException(e.getXPathException());
}
}
/**
* Perform the query, sending the results to a specified destination.
* This method must not be used with an updating query.
* This method is designed for use with a query that produces a single node (typically
* a document node or element node) as its result. If the query produces multiple nodes,
* the effect depends on the kind of destination. For example, if the result is an
* XdmDestination
, only the last of the nodes will be accessible.
*
* @param destination The destination where the result document will be sent
* @throws net.sf.saxon.s9api.SaxonApiException
* if any dynamic error occurs during the query
* @throws IllegalStateException if this is an updating query
*/
public void run(Destination destination) throws SaxonApiException {
if (expression.isUpdateQuery()) {
throw new IllegalStateException("Query is updating");
}
try {
Receiver out = getDestinationReceiver(destination);
expression.run(context, out, null);
destination.closeAndNotify();
} catch (TransformerException e) {
throw new SaxonApiException(e);
}
}
/**
* Perform the query in streaming mode, sending the results to a specified destination.
* This method must not be used with an updating query.
* This method is designed for use with a query that produces a single node (typically
* a document node or element node) as its result. If the query produces multiple nodes,
* the effect depends on the kind of destination. For example, if the result is an
* XdmDestination
, only the last of the nodes will be accessible.
*
* @param source the input stream whose document node will act as the initial context item.
* Should be a SAXSource or StreamSource
* @param destination The destination where the result document will be sent
* @throws net.sf.saxon.s9api.SaxonApiException
* if any dynamic error occurs during the query
* @throws IllegalStateException if this is an updating query
*/
public void runStreamed(Source source, Destination destination) throws SaxonApiException {
if (expression.isUpdateQuery()) {
throw new IllegalStateException("Query is updating; cannot run with streaming");
}
Configuration config = context.getConfiguration();
if (config.isTiming()) {
String systemId = source.getSystemId();
if (systemId == null) {
systemId = "";
}
config.getLogger().info("Processing streamed input " + systemId);
}
try {
Receiver receiver = getDestinationReceiver(destination);
expression.runStreamed(context, source, receiver, null);
destination.closeAndNotify();
} catch (TransformerException e) {
throw new SaxonApiException(e);
}
}
/**
* Perform the query, returning the results as an XdmValue. This method
* must not be used with an updating query
*
* @return an XdmValue representing the results of the query
* @throws SaxonApiException if the query fails with a dynamic error
* @throws IllegalStateException if this is an updating query
*/
public XdmValue evaluate() throws SaxonApiException {
if (expression.isUpdateQuery()) {
throw new IllegalStateException("Query is updating");
}
try {
SequenceIterator iter = expression.iterator(context);
return XdmValue.wrap(SequenceTool.toGroundedValue(iter));
} catch (UncheckedXPathException e) {
throw new SaxonApiException(e.getXPathException());
} catch (XPathException e) {
throw new SaxonApiException(e);
}
}
/**
* Evaluate the XQuery expression, returning the result as an XdmItem
(that is,
* a single node or atomic value).
*
* @return an XdmItem
representing the result of the query, or null if the query
* returns an empty sequence. If the expression returns a sequence of more than one item,
* any items after the first are ignored.
* @throws SaxonApiException if a dynamic error occurs during the query evaluation.
* @since 9.2
*/
public XdmItem evaluateSingle() throws SaxonApiException {
try {
SequenceIterator iter = expression.iterator(context);
Item next = iter.next();
return next == null ? null : (XdmItem) XdmValue.wrap(next);
} catch (UncheckedXPathException e) {
throw new SaxonApiException(e.getXPathException());
} catch (XPathException e) {
throw new SaxonApiException(e);
}
}
/**
* Evaluate the query, and return an iterator over its results.
* This method must not be used with an updating query.
*
* @return an Iterator<XdmItem>. The XdmSequenceIterator class is a standard Java Iterator with an additional close()
* method, which should be called to release resources if the client does not intend to read any more items
* from the iterator.
*
* @throws SaxonApiUncheckedException if a dynamic error is detected while constructing the iterator.
* It is also possible for an SaxonApiUncheckedException to be thrown by the hasNext() method of the
* returned iterator if a dynamic error occurs while evaluating the result sequence.
* @throws IllegalStateException if this is an updating query
* @since 9.5.1.5. Previously the method returned Iterator<XdmItem>; the signature changed in Saxon 9.5.1.5 for reasons
* described in bug 2016.
*/
@Override
public XdmSequenceIterator iterator() throws SaxonApiUncheckedException {
if (expression.isUpdateQuery()) {
throw new IllegalStateException("Query is updating");
}
try {
return new XdmSequenceIterator<>(expression.iterator(context));
} catch (XPathException e) {
throw new SaxonApiUncheckedException(e);
}
}
/**
* Evaluate the query, returning the result as an Stream
.
*
* @return A stream delivering the results of the query.
* Each object in this stream will be an instance of XdmItem
. Note
* that the expression may be evaluated lazily, which means that a successful response
* from this method does not imply that the expression has executed successfully: failures
* may be reported later while retrieving items from the iterator.
* @throws SaxonApiUncheckedException if a dynamic error occurs during XPath evaluation that
* can be detected at this point.
*/
public XdmStream stream() throws SaxonApiUncheckedException {
return iterator().stream();
}
private Receiver getDestinationReceiver(Destination destination) throws SaxonApiException {
Executable exec = expression.getExecutable();
PipelineConfiguration pipe = expression.getConfiguration().makePipelineConfiguration();
Receiver out = destination.getReceiver(pipe, exec.getPrimarySerializationProperties());
if (Configuration.isAssertionsEnabled()) {
return new RegularSequenceChecker(out, true);
} else {
return out;
}
}
/**
* Return a Receiver which can be used to supply the principal source document for the transformation.
* This method is intended primarily for internal use, though it can also
* be called by a user application that wishes to feed events into the query engine.
* Saxon calls this method to obtain a Receiver, to which it then sends
* a sequence of events representing the content of an XML document. This method is provided so that
* XQueryEvaluator
implements Destination
, allowing one transformation
* to receive the results of another in a pipeline.
* Note that when an XQueryEvaluator
is used as a Destination
, the initial
* context node set on that XQueryEvaluator
(using {@link #setSource(javax.xml.transform.Source)}) is ignored.
*
* @param pipe The Saxon configuration. This is supplied so that the destination can
* use information from the configuration (for example, a reference to the name pool)
* to construct or configure the returned Receiver.
* @param params the output serialization properties
* @return the Receiver to which events are to be sent.
* @throws SaxonApiException if the Receiver cannot be created
* @throws IllegalStateException if no Destination has been supplied
*/
@Override
public Receiver getReceiver(PipelineConfiguration pipe, SerializationProperties params) throws SaxonApiException {
if (destination == null) {
throw new IllegalStateException("No destination has been supplied");
}
try {
if (controller == null) {
controller = expression.newController(context);
} else {
context.initializeController(controller);
}
} catch (XPathException e) {
throw new SaxonApiException(e);
}
sourceTreeBuilder = controller.makeBuilder();
sourceTreeBuilder.setDurability(Durability.LASTING);
if (sourceTreeBuilder instanceof TinyBuilder) {
((TinyBuilder) sourceTreeBuilder).setStatistics(context.getConfiguration().getTreeStatistics().SOURCE_DOCUMENT_STATISTICS);
}
Receiver out = controller.makeStripper(sourceTreeBuilder);
SequenceNormalizer sn = params.makeSequenceNormalizer(out);
sn.onClose(() -> {
NodeInfo doc = sourceTreeBuilder.getCurrentRoot();
if (doc == null) {
throw new SaxonApiException("No source document has been built by the previous pipeline stage");
}
doc.getTreeInfo().setSpaceStrippingRule(controller.getSpaceStrippingRule());
setSource(doc.asActiveSource());
sourceTreeBuilder = null;
run(destination);
destination.closeAndNotify();
});
return sn;
}
/**
* Close this destination, allowing resources to be released. Used when this XQueryEvaluator
is acting
* as the destination of another transformation or query. Saxon calls this method when it has finished writing
* to the destination.
*/
@Override
public void close() throws SaxonApiException {
}
/**
* After executing an updating query using the {@link #run()} method, iterate over the root
* nodes of the documents updated by the query.
* The results remain available until a new query is executed. This method returns the results
* of the most recently executed query. It does not consume the results.
*
* @return an iterator over the root nodes of documents (or other trees) that were updated by the query
* @since 9.1
*/
public Iterator getUpdatedDocuments() {
return updatedDocuments.iterator();
}
/**
* Call a global user-defined function in the compiled query.
* If this is called more than once (to evaluate the same function repeatedly with different arguments,
* or to evaluate different functions) then the sequence of evaluations uses the same values of global
* variables including external variables (query parameters); the effect of any changes made to query parameters
* between calls is undefined.
*
* @param function The name of the function to be called
* @param arguments The values of the arguments to be supplied to the function. If necessary
* these are converted to the required type by applying the function conversion rules.
* @return the result of the function call as an XdmValue.
* @throws SaxonApiException if no function has been defined with the given name and arity;
* or if any of the arguments does not match its required type according to the function
* signature; or if a dynamic error occurs in evaluating the function.
* @since 9.3. Changed in 9.6 to apply the function conversion rules to the supplied arguments. Changed in 11.0
* to use varArgs.
*/
public XdmValue callFunction(QName function, XdmValue... arguments) throws SaxonApiException {
final UserFunction fn = expression.getMainModule().getUserDefinedFunction(
function.getNamespaceUri(), function.getLocalName(), arguments.length);
if (fn == null) {
throw new SaxonApiException("No function with name " + function.getEQName() +
" and arity " + arguments.length + " has been declared in the query");
}
try {
if (controller == null) {
controller = expression.newController(context);
} else {
context.initializeController(controller);
}
Configuration config = processor.getUnderlyingConfiguration();
TypeHierarchy th = config.getTypeHierarchy();
Sequence[] vr = SequenceTool.makeSequenceArray(arguments.length);
for (int i = 0; i < arguments.length; i++) {
net.sf.saxon.value.SequenceType type = fn.getParameterDefinitions()[i].getRequiredType();
GroundedValue gVal = arguments[i].getUnderlyingValue();
if (!type.matches(gVal, th)) {
final int pos = i;
Supplier role =
() -> new RoleDiagnostic(RoleDiagnostic.FUNCTION, function.getStructuredQName().getDisplayName(), pos);
gVal = th.applyFunctionConversionRules(gVal, type, role, Loc.NONE);
}
vr[i] = gVal;
}
Sequence result = fn.call(vr, controller);
return XdmValue.wrap(result);
} catch (XPathException e) {
throw new SaxonApiException(e);
}
}
/**
* Get the underlying dynamic context object. This provides an escape hatch to the underlying
* implementation classes, which contain methods that may change from one release to another.
*
* @return the underlying object representing the dynamic context for query execution
*/
public DynamicQueryContext getUnderlyingQueryContext() {
return context;
}
}