diff --git a/exist-core/pom.xml b/exist-core/pom.xml
index 2e5b76faa4..77cef9b12f 100644
--- a/exist-core/pom.xml
+++ b/exist-core/pom.xml
@@ -737,6 +737,7 @@
project-suppression.xmlsrc/test/xquery/pi.xqmsrc/test/xquery/securitymanager/acl.xqm
+ src/test/java/xquery/xquery3/transform/XQuery3TransformTests.javasrc/main/java/org/exist/dom/memtree/DocumentTypeImpl.javasrc/test/resources/org/exist/dom/memtree/simple.xhtmlsrc/main/java/org/exist/dom/memtree/reference/AbstractReferenceCharacterData.java
@@ -755,7 +756,9 @@
src/main/java/org/exist/storage/io/VariableByteFilterInputStream.javasrc/main/java/org/exist/storage/io/VariableByteFilterOutputStream.javasrc/main/java/org/exist/storage/io/VariableByteOutput.java
+ src/main/java/org/exist/storage/serializers/FeatureKeys.javasrc/test/java/org/exist/storage/serializers/NativeSerializerTest.java
+ src/main/java/org/exist/storage/serializers/NodeValueInputSource.javasrc/main/java/org/exist/util/ByteOrderMark.javasrc/main/java/org/exist/util/JREUtil.javasrc/main/java/org/exist/util/OSUtil.java
@@ -783,10 +786,11 @@
src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.javasrc/test/java/org/exist/xquery/functions/securitymanager/AccountMetadataFunctionsTest.javasrc/test/java/org/exist/xquery/functions/securitymanager/SecurityManagerTestUtil.java
+ src/main/java/org/exist/xquery/functions/system/FunctionAvailable.javasrc/test/java/org/exist/xquery/functions/system/GetMainModuleLoadPathTest.javasrc/main/java/org/exist/xquery/functions/system/GetModuleLoadPath.javasrc/test/java/org/exist/xquery/functions/system/GetModuleLoadPathTest.java
- src/main/java/org/exist/xquery/functions/system/FunctionAvailable.java
+ src/main/java/org/exist/xquery/functions/transform/Transform.javasrc/test/java/org/exist/xquery/functions/xmldb/XMLDBStoreTest.javasrc/test/java/org/exist/xquery/functions/xquery3/SerializeTest.javasrc/main/java/org/exist/xquery/value/ArrayWrapper.java
@@ -820,9 +824,11 @@
src/test/xquery/maps/maps.xqmsrc/test/xquery/numbers/format-numbers.xqlsrc/test/xquery/util/util.xml
+ src/test/xquery/xinclude/xinclude.xmlsrc/test/xquery/xquery3/parse-xml.xqmsrc/test/xquery/xquery3/serialize.xqlsrc/test/xquery/xquery3/xml-to-json.xql
+ src/test/java/xquery/xquery3/XQuery3Tests.javasrc/main/java/org/exist/Indexer.javasrc/test/java/org/exist/Indexer2Test.javasrc/test/java/org/exist/Indexer3Test.java
@@ -1376,6 +1382,7 @@
src/main/java/org/exist/xquery/functions/fn/FnInnerMost.javasrc/main/java/org/exist/xquery/functions/fn/FnModule.javasrc/main/java/org/exist/xquery/functions/fn/FnOuterMost.java
+ src/main/java/org/exist/xquery/functions/fn/FnTransform.javasrc/main/java/org/exist/xquery/functions/fn/FunAbs.javasrc/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.javasrc/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java
@@ -1424,6 +1431,7 @@
src/test/java/org/exist/xquery/functions/fn/FunLangTest.javasrc/main/java/org/exist/xquery/functions/fn/FunLast.javasrc/main/java/org/exist/xquery/functions/fn/FunLocalName.java
+ src/main/java/org/exist/xquery/functions/fn/FunMatches.javasrc/main/java/org/exist/xquery/functions/fn/FunMax.javasrc/main/java/org/exist/xquery/functions/fn/FunMin.javasrc/main/java/org/exist/xquery/functions/fn/FunName.java
@@ -1477,9 +1485,11 @@
src/main/java/org/exist/xquery/functions/fn/QNameFunctions.javasrc/main/java/org/exist/xquery/functions/fn/transform/Convert.javasrc/main/java/org/exist/xquery/functions/fn/transform/Delivery.java
+ src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.javasrc/main/java/org/exist/xquery/functions/fn/transform/Options.javasrc/main/java/org/exist/xquery/functions/fn/transform/Transform.javasrc/main/java/org/exist/xquery/functions/fn/transform/TreeUtils.java
+ src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.javasrc/test/java/org/exist/xquery/functions/inspect/InspectModuleTest.javasrc/main/java/org/exist/xquery/functions/integer/WordPicture.javasrc/main/java/org/exist/xquery/functions/map/MapExpr.java
@@ -1505,8 +1515,8 @@
src/main/java/org/exist/xquery/functions/system/SystemModule.javasrc/main/java/org/exist/xquery/functions/system/TriggerSystemTask.javasrc/test/resources-filtered/org/exist/xquery/functions/transform/transform-from-pkg-test.conf.xml
- src/main/java/org/exist/xquery/functions/transform/Transform.javasrc/test/java/org/exist/xquery/functions/transform/TransformFromPkgTest.java
+ src/main/java/org/exist/xquery/functions/transform/TransformModule.javasrc/test/java/org/exist/xquery/functions/transform/TransformTest.javasrc/test/java/org/exist/xquery/functions/util/Base64FunctionsTest.javasrc/test/java/org/exist/xquery/functions/util/BaseConverterTest.java
@@ -1591,7 +1601,10 @@
src/main/java/org/exist/xquery/value/ValueSequence.javasrc/test/java/org/exist/xquery/value/YearMonthDurationTest.javasrc/main/java/org/exist/xquery/value/YearMonthDurationValue.java
+ src/main/java/org/exist/xslt/EXistDbInputSource.javasrc/main/java/org/exist/xslt/EXistURIResolver.java
+ src/main/java/org/exist/xslt/SaxonConfiguration.java
+ src/test/java/org/exist/xslt/SaxonConfigurationTest.javasrc/main/java/org/exist/xslt/XsltURIResolverHelper.javasrc/main/java/org/exist/xupdate/Append.javasrc/main/java/org/exist/xupdate/Conditional.java
@@ -1647,6 +1660,7 @@
src/test/xquery/numbers/format-numbers.xqlsrc/test/xquery/securitymanager/acl.xqmsrc/test/xquery/util/util.xml
+ src/test/xquery/xinclude/xinclude.xmlsrc/test/xquery/xqsuite/xqsuite-assertions-dynamic.xqmsrc/test/xquery/xqsuite/xqsuite-assertions-inline.xqmsrc/test/xquery/xqsuite/xqsuite-assertions.resources.xqm.ignore
@@ -1655,6 +1669,8 @@
src/test/xquery/xquery3/postfix-expr.xqmsrc/test/xquery/xquery3/serialize.xqlsrc/test/xquery/xquery3/xml-to-json.xql
+ src/test/java/xquery/xquery3/XQuery3Tests.java
+ src/test/java/xquery/xquery3/transform/XQuery3TransformTests.javasrc/main/java/org/exist/Indexer.javasrc/test/java/org/exist/Indexer2Test.javasrc/test/java/org/exist/Indexer3Test.java
@@ -2022,7 +2038,9 @@
src/test/java/org/exist/storage/lock/ProtectedModeTest.javasrc/main/java/org/exist/storage/recovery/RecoveryManager.javasrc/main/java/org/exist/storage/serializers/EXistOutputKeys.java
+ src/main/java/org/exist/storage/serializers/FeatureKeys.javasrc/test/java/org/exist/storage/serializers/NativeSerializerTest.java
+ src/main/java/org/exist/storage/serializers/NodeValueInputSource.javasrc/main/java/org/exist/storage/serializers/Serializer.javasrc/main/java/org/exist/storage/serializers/XIncludeFilter.javasrc/test/resources-filtered/org/exist/storage/statistics/conf.xml
@@ -2320,6 +2338,7 @@
src/main/java/org/exist/xquery/functions/fn/FnInnerMost.javasrc/main/java/org/exist/xquery/functions/fn/FnModule.javasrc/main/java/org/exist/xquery/functions/fn/FnOuterMost.java
+ src/main/java/org/exist/xquery/functions/fn/FnTransform.javasrc/main/java/org/exist/xquery/functions/fn/FunAbs.javasrc/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.javasrc/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java
@@ -2369,6 +2388,7 @@
src/test/java/org/exist/xquery/functions/fn/FunLangTest.javasrc/main/java/org/exist/xquery/functions/fn/FunLast.javasrc/main/java/org/exist/xquery/functions/fn/FunLocalName.java
+ src/main/java/org/exist/xquery/functions/fn/FunMatches.javasrc/main/java/org/exist/xquery/functions/fn/FunMax.javasrc/main/java/org/exist/xquery/functions/fn/FunMin.javasrc/main/java/org/exist/xquery/functions/fn/FunName.java
@@ -2426,9 +2446,11 @@
src/test/java/org/exist/xquery/functions/fn/transform/ConvertTest.javasrc/main/java/org/exist/xquery/functions/fn/transform/Delivery.javasrc/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java
+ src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.javasrc/main/java/org/exist/xquery/functions/fn/transform/Options.javasrc/main/java/org/exist/xquery/functions/fn/transform/Transform.javasrc/main/java/org/exist/xquery/functions/fn/transform/TreeUtils.java
+ src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.javasrc/test/java/org/exist/xquery/functions/inspect/InspectModuleTest.javasrc/main/java/org/exist/xquery/functions/integer/WordPicture.javasrc/main/java/org/exist/xquery/functions/map/MapExpr.java
@@ -2465,6 +2487,7 @@
src/test/resources-filtered/org/exist/xquery/functions/transform/transform-from-pkg-test.conf.xmlsrc/main/java/org/exist/xquery/functions/transform/Transform.javasrc/test/java/org/exist/xquery/functions/transform/TransformFromPkgTest.java
+ src/main/java/org/exist/xquery/functions/transform/TransformModule.javasrc/test/java/org/exist/xquery/functions/transform/TransformTest.javasrc/test/java/org/exist/xquery/functions/util/Base64FunctionsTest.javasrc/test/java/org/exist/xquery/functions/util/BaseConverterTest.java
@@ -2566,7 +2589,10 @@
src/main/java/org/exist/xquery/value/ValueSequence.javasrc/test/java/org/exist/xquery/value/YearMonthDurationTest.javasrc/main/java/org/exist/xquery/value/YearMonthDurationValue.java
+ src/main/java/org/exist/xslt/EXistDbInputSource.javasrc/main/java/org/exist/xslt/EXistURIResolver.java
+ src/main/java/org/exist/xslt/SaxonConfiguration.java
+ src/test/java/org/exist/xslt/SaxonConfigurationTest.javasrc/main/java/org/exist/xslt/XsltURIResolverHelper.javasrc/main/java/org/exist/xupdate/Append.javasrc/main/java/org/exist/xupdate/Conditional.java
diff --git a/exist-core/src/main/java/org/exist/Namespaces.java b/exist-core/src/main/java/org/exist/Namespaces.java
index 4a284ffb9a..4e895a0ff9 100644
--- a/exist-core/src/main/java/org/exist/Namespaces.java
+++ b/exist-core/src/main/java/org/exist/Namespaces.java
@@ -91,6 +91,7 @@ public interface Namespaces {
String EXIST_JAVA_BINDING_NS = "http://exist.sourceforge.net/NS/exist/java-binding";
String EXIST_JAVA_BINDING_NS_PREFIX = "java";
+ String EXIST_FEATURE_NS = EXIST_NS + "/feature";
String XML_NS = XMLConstants.XML_NS_URI;
String XMLNS_NS = XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
diff --git a/exist-core/src/main/java/org/exist/dom/memtree/reference/AbstractReferenceNodeImpl.java b/exist-core/src/main/java/org/exist/dom/memtree/reference/AbstractReferenceNodeImpl.java
index 80109afdab..e9878b505c 100644
--- a/exist-core/src/main/java/org/exist/dom/memtree/reference/AbstractReferenceNodeImpl.java
+++ b/exist-core/src/main/java/org/exist/dom/memtree/reference/AbstractReferenceNodeImpl.java
@@ -23,7 +23,6 @@
import org.exist.dom.QName;
import org.exist.dom.memtree.DocumentImpl;
import org.exist.dom.memtree.NodeImpl;
-import org.exist.dom.persistent.AttrImpl;
import org.exist.dom.persistent.NodeProxy;
import org.exist.xquery.Expression;
import org.exist.xquery.NodeTest;
diff --git a/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java b/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java
index 93a0832368..019eb4cddb 100644
--- a/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java
+++ b/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java
@@ -230,7 +230,7 @@ public NodeProxy(final DocumentImpl doc, final NodeId nodeId, final short nodeTy
public NodeProxy(final Expression expression, final DocumentImpl doc, final NodeId nodeId, final short nodeType, final long address) {
this.expression = (expression == null && doc != null) ? doc.getExpression() : expression;
this.doc = doc;
- this.nodeType = nodeType;
+ this.nodeType = nodeType == UNKNOWN_NODE_TYPE && NodeId.DOCUMENT_NODE.equals(nodeId) ? Node.DOCUMENT_NODE : nodeType;
this.internalAddress = address;
this.nodeId = nodeId;
}
diff --git a/exist-core/src/main/java/org/exist/storage/BrokerPool.java b/exist-core/src/main/java/org/exist/storage/BrokerPool.java
index 97df5142eb..14351e6f78 100644
--- a/exist-core/src/main/java/org/exist/storage/BrokerPool.java
+++ b/exist-core/src/main/java/org/exist/storage/BrokerPool.java
@@ -47,7 +47,6 @@
import com.evolvedbinary.j8fu.fsm.AtomicFSM;
import com.evolvedbinary.j8fu.fsm.FSM;
-import com.evolvedbinary.j8fu.lazy.AtomicLazyVal;
import net.jcip.annotations.ThreadSafe;
import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.logging.log4j.LogManager;
@@ -142,8 +141,6 @@ public class BrokerPool extends BrokerPools implements BrokerPoolConstants, Data
private final XQuery xqueryService = new XQuery();
- private AtomicLazyVal saxonConfiguration = new AtomicLazyVal<>(() -> SaxonConfiguration.loadConfiguration(this));
-
//TODO : make it non-static since every database instance may have its own policy.
//TODO : make a default value that could be overwritten by the configuration
// WM: this is only used by junit tests to test the recovery process.
@@ -1957,14 +1954,6 @@ public void registerCollectionTrigger(final Class extends CollectionTrigger> c
collectionTriggers.add(new CollectionTriggerProxy(clazz));
}
- public net.sf.saxon.Configuration getSaxonConfiguration() {
- return saxonConfiguration.get().getConfiguration();
- }
-
- public net.sf.saxon.s9api.Processor getSaxonProcessor() {
- return saxonConfiguration.get().getProcessor();
- }
-
/**
* Represents a change involving {@link BrokerPool#inactiveBrokers}
* or {@link BrokerPool#activeBrokers} or {@link DBBroker#getReferenceCount}
diff --git a/exist-core/src/main/java/org/exist/storage/serializers/FeatureKeys.java b/exist-core/src/main/java/org/exist/storage/serializers/FeatureKeys.java
new file mode 100644
index 0000000000..f040b7adb0
--- /dev/null
+++ b/exist-core/src/main/java/org/exist/storage/serializers/FeatureKeys.java
@@ -0,0 +1,41 @@
+/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.exist.storage.serializers;
+
+/**
+ * Elemental Feature Keys for {@link Serializer}.
+ *
+ * @author Adam Retter
+ */
+public interface FeatureKeys {
+
+ String NS = "http://ns.elemental.xyz/feature";
+
+ /**
+ * Enable or disable XInclude processing in {@link Serializer}.
+ */
+ String EXPAND_XINCLUDES = NS + "/" + EXistOutputKeys.EXPAND_XINCLUDES;
+
+ /**
+ * Set the path for resolving XInclude resources in {@link Serializer}.
+ */
+ String XINCLUDE_PATH = NS + "/" + EXistOutputKeys.XINCLUDE_PATH;
+}
diff --git a/exist-core/src/main/java/org/exist/storage/serializers/NodeValueInputSource.java b/exist-core/src/main/java/org/exist/storage/serializers/NodeValueInputSource.java
new file mode 100644
index 0000000000..220e9b7a8a
--- /dev/null
+++ b/exist-core/src/main/java/org/exist/storage/serializers/NodeValueInputSource.java
@@ -0,0 +1,85 @@
+/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.exist.storage.serializers;
+
+import org.exist.xquery.value.NodeValue;
+import org.xml.sax.InputSource;
+
+import java.io.InputStream;
+import java.io.Reader;
+
+/**
+ * Provides a NodeValue to {@link Serializer#parse(InputSource)}.
+ * This is not a general purpose {@link InputSource} implementation,
+ * it should only be used with {@link Serializer}.
+ *
+ * @author Adam Retter
+ */
+public class NodeValueInputSource extends InputSource {
+
+ private final NodeValue nodeValue;
+
+ /**
+ * Construct a new NodeValueInputSource where the systemId
+ * will be taken from the Base URI of the node.
+ *
+ * @param nodeValue the NodeValue.
+ */
+ public NodeValueInputSource(final NodeValue nodeValue) {
+ this(nodeValue, nodeValue.getNode().getBaseURI());
+ }
+
+ /**
+ * Construct a new NodeValueInputSource where the systemId
+ * is specified.
+ *
+ * @param nodeValue the NodeValue.
+ * @param systemId the systemId.
+ */
+ public NodeValueInputSource(final NodeValue nodeValue, final String systemId) {
+ super(systemId);
+ this.nodeValue = nodeValue;
+ }
+
+ @Override
+ public void setByteStream(final InputStream byteStream) {
+ throw new UnsupportedOperationException("This implementation does not support byte streams");
+ }
+
+ @Override
+ public void setEncoding(final String encoding) {
+ throw new UnsupportedOperationException("This implementation does not support character encodings");
+ }
+
+ @Override
+ public void setCharacterStream(final Reader characterStream) {
+ throw new UnsupportedOperationException("This implementation does not support character streams");
+ }
+
+ /**
+ * Get the NodeValue.
+ *
+ * @return the NodeValue.
+ */
+ NodeValue getNodeValue() {
+ return nodeValue;
+ }
+}
diff --git a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java
index 084c1c9a29..a52f031f32 100644
--- a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java
+++ b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java
@@ -52,7 +52,6 @@
import java.util.*;
import javax.annotation.Nullable;
-import javax.xml.XMLConstants;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
@@ -312,6 +311,7 @@ public void setProperties(@Nullable final HashMap table)
}
}
+ @Override
public void setProperty(final String prop, final Object value)
throws SAXNotRecognizedException, SAXNotSupportedException {
switch (prop) {
@@ -378,10 +378,12 @@ protected void applyXSLHandler(final Writer writer) {
*
* @return The entityResolver value
*/
+ @Override
public @Nullable EntityResolver getEntityResolver() {
return entityResolver;
}
+ @Override
public @Nullable ErrorHandler getErrorHandler() {
return errorHandler;
}
@@ -405,6 +407,7 @@ public void setUser(final Subject user) {
return user;
}
+ @Override
public boolean getFeature(final String name)
throws SAXNotRecognizedException, SAXNotSupportedException {
if (name.equals(Namespaces.SAX_NAMESPACES)
@@ -414,6 +417,7 @@ public boolean getFeature(final String name)
throw new SAXNotRecognizedException(name);
}
+ @Override
public Object getProperty(final String name)
throws SAXNotRecognizedException, SAXNotSupportedException {
if (name.equals(Namespaces.SAX_LEXICAL_HANDLER)) {
@@ -429,13 +433,20 @@ public Object getProperty(final String name)
return null;
}
+ @Override
public void parse(final InputSource input) throws IOException, SAXException {
- // only system-ids are handled
- final String doc = input.getSystemId();
- if (doc == null) {
- throw new SAXException("source is not an eXist document");
+ if (input instanceof NodeValueInputSource) {
+ final NodeValue nodeValue = ((NodeValueInputSource) input).getNodeValue();
+ toSAX(nodeValue);
+
+ } else {
+ // fallback to trying to parse from the systemId
+ @Nullable final String systemId = input.getSystemId();
+ if (systemId == null) {
+ throw new SAXException("Source's systemId is null");
+ }
+ parse(systemId);
}
- parse(doc);
}
protected void setDocument(final DocumentImpl doc) {
@@ -448,20 +459,26 @@ protected void setXQueryContext(@Nullable final XQueryContext context) {
}
}
+ @Override
public void parse(final String systemId) throws IOException, SAXException {
+ if (systemId == null) {
+ throw new SAXException("systemId is null");
+ }
+
try {
- // try to load document from eXist
- //TODO: this systemId came from exist, so should be an unchecked create, right?
- final DocumentImpl doc = broker.getResource(XmldbURI.create(systemId), Permission.READ);
+ // assume the systemId is a URI to a document in the database
+ @Nullable final DocumentImpl doc = broker.getResource(XmldbURI.create(systemId), Permission.READ);
if (doc == null) {
- throw new SAXException("document " + systemId + " not found in database");
- } else {
- LOG.debug("serializing {}", doc.getFileURI());
+ throw new SAXException("Document " + systemId + " not found in the database");
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Serializing {}", doc.getFileURI());
}
toSAX(doc);
} catch (final PermissionDeniedException e) {
- throw new SAXException("permission denied");
+ throw new SAXException("Permission denied to read document + " + systemId + ": " + e.getMessage(), e);
}
}
@@ -715,9 +732,19 @@ private void prepareStylesheets(final Document doc) throws TransformerConfigurat
* @param contentHandler the content handler
* @param lexicalHandler the lexical handle
*/
- public void setSAXHandlers(final ContentHandler contentHandler, final LexicalHandler lexicalHandler) {
+ public void setSAXHandlers(final ContentHandler contentHandler, @Nullable LexicalHandler lexicalHandler) {
final ReceiverToSAX toSAX = new ReceiverToSAX(contentHandler);
- toSAX.setLexicalHandler(lexicalHandler);
+
+ if (lexicalHandler != null) {
+ this.lexicalHandler = lexicalHandler;
+ }
+ if (this.lexicalHandler == null && contentHandler instanceof LexicalHandler) {
+ this.lexicalHandler = (LexicalHandler) contentHandler;
+ }
+ if (this.lexicalHandler != null) {
+ toSAX.setLexicalHandler(this.lexicalHandler);
+ }
+
if ("yes".equals(getProperty(EXistOutputKeys.EXPAND_XINCLUDES, "yes"))) {
xinclude.setReceiver(toSAX);
receiver = xinclude;
@@ -758,6 +785,7 @@ public void setContentHandler(final ContentHandler handler) {
*
* @param entityResolver The new entityResolver value
*/
+ @Override
public void setEntityResolver(@Nullable final EntityResolver entityResolver) {
this.entityResolver = entityResolver;
}
@@ -767,6 +795,7 @@ public void setEntityResolver(@Nullable final EntityResolver entityResolver) {
*
* @param errorHandler The new errorHandler value
*/
+ @Override
public void setErrorHandler(@Nullable final ErrorHandler errorHandler) {
this.errorHandler = errorHandler;
}
@@ -779,11 +808,17 @@ public void setErrorHandler(@Nullable final ErrorHandler errorHandler) {
* @throws SAXNotRecognizedException Description of the Exception
* @throws SAXNotSupportedException Description of the Exception
*/
- public void setFeature(final String name, final boolean value)
- throws SAXNotRecognizedException, SAXNotSupportedException {
- if (name.equals(Namespaces.SAX_NAMESPACES) || name.equals(Namespaces.SAX_NAMESPACES_PREFIXES)) {
- throw new SAXNotSupportedException(name);
+ @Override
+ public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
+ if (Namespaces.SAX_NAMESPACES.equals(name) || Namespaces.SAX_NAMESPACES_PREFIXES.equals(name)) {
+ return;
}
+
+ if (FeatureKeys.EXPAND_XINCLUDES.equals(name)) {
+ setProperty(EXistOutputKeys.EXPAND_XINCLUDES, value ? "yes" : "no");
+ return;
+ }
+
throw new SAXNotRecognizedException(name);
}
diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java
index 889783c6b2..4ff5a0cbe1 100644
--- a/exist-core/src/main/java/org/exist/util/Configuration.java
+++ b/exist-core/src/main/java/org/exist/util/Configuration.java
@@ -62,6 +62,7 @@
import org.exist.xquery.Expression;
import org.exist.xquery.PerformanceStats;
import org.exist.xquery.XQueryWatchDog;
+import org.exist.xslt.SaxonConfiguration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
diff --git a/exist-core/src/main/java/org/exist/util/SaxonConfiguration.java b/exist-core/src/main/java/org/exist/util/SaxonConfiguration.java
deleted file mode 100644
index bd2ccd2287..0000000000
--- a/exist-core/src/main/java/org/exist/util/SaxonConfiguration.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * eXist-db Open Source Native XML Database
- * Copyright (C) 2001 The eXist-db Authors
- *
- * info@exist-db.org
- * http://www.exist-db.org
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-package org.exist.util;
-
-import net.jcip.annotations.ThreadSafe;
-import net.sf.saxon.s9api.Processor;
-import net.sf.saxon.trans.XPathException;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.exist.storage.BrokerPool;
-
-import javax.xml.transform.stream.StreamSource;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Optional;
-
-@ThreadSafe
-public final class SaxonConfiguration {
-
- private final static Logger LOG = LogManager.getLogger(SaxonConfiguration.class);
-
- public static final String SAXON_CONFIGURATION_ELEMENT_NAME = "saxon";
- public static final String SAXON_CONFIGURATION_FILE_ATTRIBUTE = "configuration-file";
- public static final String SAXON_CONFIGURATION_FILE_PROPERTY = "saxon.configuration";
- private static final String SAXON_DEFAULT_SAXON_CONFIG_FILE = "saxon-config.xml";
-
- /**
- * Holds the Saxon configuration specific to a single broker pool
- */
- private final net.sf.saxon.Configuration configuration;
- private final Processor processor;
-
- private SaxonConfiguration(final net.sf.saxon.Configuration configuration) {
- this.configuration = configuration;
- this.processor = new Processor(configuration);
- //TODO (AP) This is a better place to configure URI/Resource resolution for Saxon within eXist
- //At present the configuration for Saxon to resolve xmldb:exist: URIs is restricted to fn:transform
- }
-
- /**
- * Get the Saxon API's {@link net.sf.saxon.Configuration} object.
- *
- * @return Saxon internal configuration object
- */
- public net.sf.saxon.Configuration getConfiguration() {
- return configuration;
- }
-
- /**
- * Get the Saxon API's {@link Processor} through which Saxon operations
- * such as transformation can be effected.
- *
- * @return the Saxon {@link Processor} associated with the configuration.
- */
- public Processor getProcessor() {
- return processor;
- }
-
- /**
- * Load the Saxon {@link net.sf.saxon.Configuration} from a configuration file when it is first needed;
- * if we cannot find a configuration file (and license) to give to Saxon, it (Saxon) may still be able to find
- * something by searching in more "well-known to Saxon" locations.
- *
- * @return a freshly loaded Saxon configuration
- */
- public static SaxonConfiguration loadConfiguration(final BrokerPool brokerPool) {
-
- final var existConfiguration = brokerPool.getConfiguration();
- final var saxonConfigFile = getSaxonConfigFile(existConfiguration);
- Optional saxonConfiguration = Optional.empty();
- if (saxonConfigFile.isPresent()) {
- saxonConfiguration = readSaxonConfigurationFile(saxonConfigFile.get());
- }
-
- if (saxonConfiguration.isEmpty()) {
- saxonConfiguration = Optional.of(net.sf.saxon.Configuration.newConfiguration());
- }
-
- if (saxonConfigFile.isEmpty()) {
- LOG.warn("eXist could not find any Saxon configuration:\n" +
- "No Saxon configuration file in configuration item " + SAXON_CONFIGURATION_FILE_PROPERTY + "\n" +
- "No default eXist Saxon configuration file " + SAXON_DEFAULT_SAXON_CONFIG_FILE);
- }
-
- saxonConfiguration.ifPresent(SaxonConfiguration::reportLicensedFeatures);
-
- return new SaxonConfiguration(saxonConfiguration.get());
- }
-
- static private Optional readSaxonConfigurationFile(final Path saxonConfigFile) {
- try {
- return Optional.of(net.sf.saxon.Configuration.readConfiguration(
- new StreamSource(Files.newInputStream(saxonConfigFile))));
- } catch (final XPathException | IOException e) {
- LOG.warn("Saxon could not read the configuration file: " + saxonConfigFile +
- ", with error: " + e.getMessage(), e);
- } catch (RuntimeException runtimeException) {
- if (runtimeException.getCause() instanceof ClassNotFoundException e) {
- LOG.warn("Saxon could not honour the configuration file: " + saxonConfigFile +
- ", with class not found error: " + e.getMessage() + ". You may need to install the SaxonPE or SaxonEE JAR in eXist.");
- } else {
- throw runtimeException;
- }
- }
- return Optional.empty();
- }
-
- static private void reportLicensedFeatures(final net.sf.saxon.Configuration configuration) {
- configuration.displayLicenseMessage();
-
- final var sb = new StringBuilder();
- if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.SCHEMA_VALIDATION)) {
- sb.append(" SCHEMA_VALIDATION");
- }
- if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.ENTERPRISE_XSLT)) {
- sb.append(" ENTERPRISE_XSLT");
- }
- if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.ENTERPRISE_XQUERY)) {
- sb.append(" ENTERPRISE_XQUERY");
- }
- if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.PROFESSIONAL_EDITION)) {
- sb.append(" PROFESSIONAL_EDITION");
- }
- if (sb.isEmpty()) {
- LOG.info("Saxon - no licensed features reported.");
- } else {
- LOG.info("Saxon - licensed features are" + sb + ".");
- }
- }
-
- /**
- * Resolve a possibly relative configuration file;
- * if it is relative, it is relative to the current exist configuration (conf.xml)
- *
- * @param existConfiguration configuration to which this file may be relative
- * @param filename the file we are trying to resolve
- * @return the input file, if it is absolute. a file relative to conf.xml, if the input file is relative
- */
- private static Path resolveConfigurationFile(final Configuration existConfiguration, final String filename) {
- final var configurationFile = Paths.get(filename);
- if (configurationFile.isAbsolute()) {
- return configurationFile;
- }
-
- final var configPath = existConfiguration.getConfigFilePath();
- return configPath.map(p -> p.getParent().resolve(configurationFile)).orElse(configurationFile);
- }
-
- private static Optional getSaxonConfigFile(final Configuration existConfiguration) {
- if (existConfiguration.getProperty(SAXON_CONFIGURATION_FILE_PROPERTY) instanceof String saxonConfigurationFile) {
- final var configurationFile = resolveConfigurationFile(existConfiguration, saxonConfigurationFile);
- if (Files.isReadable(configurationFile)) {
- return Optional.of(configurationFile);
- } else {
- LOG.warn("Configuration item " + SAXON_CONFIGURATION_FILE_PROPERTY + " : " + configurationFile +
- " does not refer to a readable file. Continuing search for Saxon configuration.");
- }
- }
-
- final var configurationFile = resolveConfigurationFile(existConfiguration, SAXON_DEFAULT_SAXON_CONFIG_FILE);
- if (Files.isReadable(configurationFile)) {
- return Optional.of(configurationFile);
- }
-
- return Optional.empty();
- }
-}
diff --git a/exist-core/src/main/java/org/exist/xquery/Context.java b/exist-core/src/main/java/org/exist/xquery/Context.java
index 8bc528d517..841aeb78b0 100644
--- a/exist-core/src/main/java/org/exist/xquery/Context.java
+++ b/exist-core/src/main/java/org/exist/xquery/Context.java
@@ -298,7 +298,7 @@ public interface Context {
*/
Collator getCollator(String uri, ErrorCodes.ErrorCode errorCode) throws XPathException;
- Collator getDefaultCollator();
+ @Nullable Collator getDefaultCollator();
/**
* Set the set of statically known documents for the current execution context.
diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
index f37e5cddcb..35399db347 100644
--- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
+++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
@@ -1148,7 +1148,7 @@ public Collator getCollator(String uri, final ErrorCodes.ErrorCode errorCode) th
}
@Override
- public Collator getDefaultCollator() {
+ public @Nullable Collator getDefaultCollator() {
return defaultCollator;
}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnTransform.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnTransform.java
index 202cd4c78a..0fdee7c0c2 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnTransform.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnTransform.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -19,16 +43,22 @@
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-
package org.exist.xquery.functions.fn;
+import net.sf.saxon.s9api.Processor;
import org.exist.xquery.BasicFunction;
import org.exist.xquery.FunctionSignature;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
+import org.exist.xquery.functions.fn.transform.Convert;
+import org.exist.xquery.functions.fn.transform.Options;
import org.exist.xquery.functions.fn.transform.Transform;
+import org.exist.xquery.functions.map.MapType;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.Type;
+import org.exist.xslt.SaxonConfiguration;
+
+import javax.annotation.Nullable;
import static org.exist.xquery.FunctionDSL.param;
import static org.exist.xquery.FunctionDSL.returnsOptMany;
@@ -61,11 +91,14 @@ public class FnTransform extends BasicFunction {
public FnTransform(final XQueryContext context, final FunctionSignature signature) {
super(context, signature);
- this.transform = new Transform(context, this);
+ this.transform = new Transform(this);
}
@Override
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
- return transform.eval(args, contextSequence);
+ final SaxonConfiguration saxonConfiguration = SaxonConfiguration.getConfiguration(getContext().getConfiguration(), null);
+ final Convert.ToSaxon toSaxon = new Convert.ToSaxon(saxonConfiguration.getProcessor());
+ final Options options = new Options(this, saxonConfiguration, toSaxon, (MapType) args[0].itemAt(0));
+ return transform.eval(saxonConfiguration, toSaxon, options, contextSequence, null);
}
}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java
index f42ba69e65..6cd539b584 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java
@@ -71,6 +71,7 @@
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.Type;
+import org.exist.xslt.SaxonConfiguration;
import org.xml.sax.helpers.AttributesImpl;
import javax.xml.XMLConstants;
@@ -173,7 +174,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
}
private void analyzeString(final MemTreeBuilder builder, final String input, String pattern, final String flags) throws XPathException {
- final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration();
+ final Configuration config = SaxonConfiguration.getConfiguration(getContext().getConfiguration(), null).getConfiguration();
final List warnings = new ArrayList<>(1);
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java
index f0e3ef9d35..7c7a791c11 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -47,6 +71,7 @@
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import net.sf.saxon.regex.RegularExpression;
+import org.exist.xslt.SaxonConfiguration;
import static org.exist.xquery.FunctionDSL.*;
import static org.exist.xquery.functions.fn.FnModule.functionSignatures;
@@ -515,8 +540,8 @@ private Sequence evalGeneric(final Sequence contextSequence, final Item contextI
private boolean matchXmlRegex(final String string, final String pattern, final String flags) throws XPathException {
try {
List warnings = new ArrayList<>(1);
- RegularExpression regex = context.getBroker().getBrokerPool()
- .getSaxonConfiguration()
+ RegularExpression regex = SaxonConfiguration.getConfiguration(getContext().getConfiguration(), null)
+ .getConfiguration()
.compileRegularExpression(pattern, flags, "XP30", warnings);
for (final String warning : warnings) {
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java
index a57fd52693..b41dc9b4cb 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java
@@ -57,6 +57,7 @@
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;
+import org.exist.xslt.SaxonConfiguration;
import static org.exist.xquery.FunctionDSL.*;
import static org.exist.xquery.regex.RegexUtil.*;
@@ -138,7 +139,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
final String pattern = args[1].itemAt(0).getStringValue();
final String replace = args[2].itemAt(0).getStringValue();
- final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration();
+ final Configuration config = SaxonConfiguration.getConfiguration(getContext().getConfiguration(), null).getConfiguration();
final List warnings = new ArrayList<>(1);
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Convert.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Convert.java
index f1e6aba1d1..16a0173c7d 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Convert.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Convert.java
@@ -79,7 +79,7 @@
* It's not clear how easy or hard that would be.
*
*/
-class Convert {
+public class Convert {
private Convert() {
super();
@@ -132,9 +132,17 @@ static NodeValue ofNode(final XdmNode xdmNode) throws XPathException {
static final private String COULD_NOT_BE_CONVERTED = " could not be converted to an eXist ";
- abstract static class ToSaxon {
+ public static class ToSaxon {
- abstract DocumentBuilder newDocumentBuilder();
+ private final Processor processor;
+
+ public ToSaxon(final Processor processor) {
+ this.processor = processor;
+ }
+
+ DocumentBuilder newDocumentBuilder() {
+ return processor.newDocumentBuilder();
+ }
static net.sf.saxon.s9api.QName of(final QName qName) {
return new net.sf.saxon.s9api.QName(qName.getPrefix() == null ? "" : qName.getPrefix(), qName.getNamespaceURI(), qName.getLocalPart());
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Options.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Options.java
index 072dbbdddc..7dacd918c8 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Options.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Options.java
@@ -47,6 +47,7 @@
import com.evolvedbinary.j8fu.tuple.Tuple2;
import io.lacuna.bifurcan.IEntry;
+import io.lacuna.bifurcan.IMap;
import net.jpountz.xxhash.XXHash64;
import net.jpountz.xxhash.XXHashFactory;
import net.sf.saxon.expr.parser.RetainedStaticContext;
@@ -54,14 +55,16 @@
import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.XdmValue;
import org.exist.dom.memtree.NamespaceNode;
+import org.exist.storage.serializers.EXistOutputKeys;
+import org.exist.storage.serializers.FeatureKeys;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.ErrorCodes;
+import org.exist.xquery.Expression;
import org.exist.xquery.XPathException;
-import org.exist.xquery.XQueryContext;
import org.exist.xquery.functions.array.ArrayType;
-import org.exist.xquery.functions.fn.FnTransform;
import org.exist.xquery.functions.map.MapType;
import org.exist.xquery.value.*;
+import org.exist.xslt.SaxonConfiguration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
@@ -98,7 +101,7 @@
* This is a bit clearer where we need an option several times,
* we know we have read it up front.
*/
-class Options {
+public class Options {
static final javax.xml.namespace.QName QN_XSL_STYLESHEET = new javax.xml.namespace.QName(XSL_NS, "stylesheet");
static final javax.xml.namespace.QName QN_VERSION = new javax.xml.namespace.QName("version");
@@ -144,27 +147,27 @@ class Options {
final Optional postProcess;
- private final XQueryContext context;
- private final FnTransform fnTransform;
+ private final Expression callingExpression;
+ final SaxonConfiguration saxonConfiguration;
private final Convert.ToSaxon toSaxon;
private final SystemProperties systemProperties;
- Options(final XQueryContext context, final FnTransform fnTransform, final Convert.ToSaxon toSaxon, final MapType options) throws XPathException {
- this.context = context;
- this.fnTransform = fnTransform;
+ public Options(final Expression callingExpression, final SaxonConfiguration saxonConfiguration, final Convert.ToSaxon toSaxon, final MapType options) throws XPathException {
+ this.callingExpression = callingExpression;
+ this.saxonConfiguration = saxonConfiguration;
this.toSaxon = toSaxon;
- this.systemProperties = new SystemProperties(context);
+ this.systemProperties = new SystemProperties(saxonConfiguration);
xsltSource = getStylesheet(options);
- stylesheetParams = Options.STYLESHEET_PARAMS.get(options).orElse(new MapType(context));
+ stylesheetParams = Options.STYLESHEET_PARAMS.get(options).orElse(new MapType(callingExpression.getContext()));
for (final IEntry entry : stylesheetParams) {
if (!(entry.key() instanceof QNameValue)) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied stylesheet-param is not a valid xs:qname: " + entry);
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Supplied stylesheet-param is not a valid xs:qname: " + entry);
}
if (entry.value() == null) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied stylesheet-param is not a valid xs:sequence: " + entry);
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Supplied stylesheet-param is not a valid xs:sequence: " + entry);
}
}
@@ -173,10 +176,10 @@ class Options {
try {
xsltVersion = XSLTVersion.fromDecimal(explicitXsltVersion.get().getValue());
if (xsltVersion.equals(V1_0) && xsltVersion.equals(V2_0) && xsltVersion.equals(V3_0)) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0001, "Supplied xslt-version is an unknown XSLT version: " + explicitXsltVersion.get());
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0001, "Supplied xslt-version is an unknown XSLT version: " + explicitXsltVersion.get());
}
} catch (final Transform.PendingException pe) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0001, "Supplied xslt-version is an unknown XSLT version: " + explicitXsltVersion.get());
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0001, "Supplied xslt-version is an unknown XSLT version: " + explicitXsltVersion.get());
}
} else {
xsltVersion = getXsltVersion(xsltSource._2);
@@ -190,7 +193,7 @@ class Options {
stylesheetBaseUri = xsltSource._1;
}
if (notNullOrEmpty(stylesheetBaseUri)) {
- resolvedStylesheetBaseURI = Optional.of(resolveURI(new AnyURIValue(stylesheetBaseUri), context.getBaseURI()));
+ resolvedStylesheetBaseURI = Optional.of(resolveURI(new AnyURIValue(stylesheetBaseUri), callingExpression.getContext().getBaseURI()));
} else {
resolvedStylesheetBaseURI = Optional.empty();
}
@@ -223,7 +226,7 @@ class Options {
serializationParams = Options.SERIALIZATION_PARAMS.get(xsltVersion, options);
- validateRequestedProperties(Options.REQUESTED_PROPERTIES.get(xsltVersion, options).orElse(new MapType(context)));
+ validateRequestedProperties(Options.REQUESTED_PROPERTIES.get(xsltVersion, options).orElse(new MapType(callingExpression.getContext())));
postProcess = Options.POST_PROCESS.get(xsltVersion, options);
@@ -244,14 +247,14 @@ private Map readParamsMap(final Optional option, final
final Map result = new HashMap<>();
- final MapType paramsMap = option.orElse(new MapType(context));
+ final MapType paramsMap = option.orElse(new MapType(callingExpression.getContext()));
for (final IEntry entry : paramsMap) {
final AtomicValue key = entry.key();
if (!(key instanceof QNameValue)) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied " + name + " is not a valid xs:qname: " + entry);
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Supplied " + name + " is not a valid xs:qname: " + entry);
}
if (entry.value() == null) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied " + name + " is not a valid xs:sequence: " + entry);
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Supplied " + name + " is not a valid xs:sequence: " + entry);
}
result.put(Convert.ToSaxon.of((QNameValue) key), toSaxon.of(entry.value()));
}
@@ -264,7 +267,7 @@ private Delivery.Format getDeliveryFormat(final XSLTVersion xsltVersion, final M
try {
format = Delivery.Format.valueOf(deliveryFormatString);
} catch (final IllegalArgumentException ie) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002,
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002,
": \"" + deliveryFormatString + "\" is not a valid " + Options.DELIVERY_FORMAT.name);
}
return format;
@@ -356,17 +359,17 @@ private void validateRequestedProperties(final MapType requestedProperties) thro
Type.MAP_ITEM,"requested-properties", V1_0, V2_0, V3_0);
private static final Option SERIALIZATION_PARAMS = new ItemOption<>(
Type.MAP_ITEM,"serialization-params", V1_0, V2_0, V3_0);
- static final Option SOURCE_NODE = new ItemOption<>(
+ public static final Option SOURCE_NODE = new ItemOption<>(
Type.NODE,"source-node", V1_0, V2_0, V3_0);
private static final Option STATIC_PARAMS = new ItemOption<>(
Type.MAP_ITEM,"static-params", V3_0);
private static final Option STYLESHEET_BASE_URI = new ItemOption<>(
Type.STRING, "stylesheet-base-uri", V1_0, V2_0, V3_0);
- static final Option STYLESHEET_LOCATION = new ItemOption<>(
+ public static final Option STYLESHEET_LOCATION = new ItemOption<>(
Type.STRING,"stylesheet-location", V1_0, V2_0, V3_0);
- static final Option STYLESHEET_NODE = new ItemOption<>(
+ public static final Option STYLESHEET_NODE = new ItemOption<>(
Type.NODE,"stylesheet-node", V1_0, V2_0, V3_0);
- private static final Option STYLESHEET_PARAMS = new ItemOption<>(
+ public static final Option STYLESHEET_PARAMS = new ItemOption<>(
Type.MAP_ITEM,"stylesheet-params", V1_0, V2_0, V3_0);
static final Option STYLESHEET_TEXT = new ItemOption<>(
Type.STRING,"stylesheet-text", V1_0, V2_0, V3_0);
@@ -374,29 +377,39 @@ private void validateRequestedProperties(final MapType requestedProperties) thro
Type.MAP_ITEM,"template-params", V3_0);
private static final Option TUNNEL_PARAMS = new ItemOption<>(
Type.MAP_ITEM,"tunnel-params", V3_0);
- private static final Option VENDOR_OPTIONS = new ItemOption<>(
+ public static final Option VENDOR_OPTIONS = new ItemOption<>(
Type.MAP_ITEM,"vendor-options", V1_0, V2_0, V3_0);
private static final Option XSLT_VERSION = new ItemOption<>(
Type.DECIMAL,"xslt-version", V1_0, V2_0, V3_0);
- abstract static class Option {
+ // Elemental vendor options
+ public static final VendorOption EXPAND_XINCLUDES = new VendorItemOption<>(
+ Type.BOOLEAN, EXistOutputKeys.EXPAND_XINCLUDES, BooleanValue.FALSE, V1_0, V2_0, V3_0);
+ public static final VendorOption XINCLUDE_PATH = new VendorItemOption<>(
+ Type.STRING, EXistOutputKeys.XINCLUDE_PATH, V1_0, V2_0, V3_0);
+
+ public abstract static class AbstractOption {
public static final XSLTVersion V1_0 = new XSLTVersion(1,0);
public static final XSLTVersion V2_0 = new XSLTVersion(2,0);
public static final XSLTVersion V3_0 = new XSLTVersion(3,0);
- protected final StringValue name;
- protected final Optional defaultValue;
+ protected final K name;
+ protected final Optional defaultValue;
protected final XSLTVersion[] appliesToVersions;
protected final int itemSubtype;
- private Option(final int itemSubtype, final String name, final Optional defaultValue, final XSLTVersion... appliesToVersions) {
- this.name = new StringValue(name);
+ private AbstractOption(final int itemSubtype, final K name, final Optional defaultValue, final XSLTVersion... appliesToVersions) {
+ this.name = name;
this.defaultValue = defaultValue;
this.appliesToVersions = appliesToVersions;
this.itemSubtype = itemSubtype;
}
- public abstract Optional get(final MapType options) throws XPathException;
+ public abstract Optional get(final MapType options) throws XPathException;
+
+ public abstract MapType set(final MapType options, final V value);
+
+ public abstract IMap set(final IMap options, final V value);
private boolean notApplicableToVersion(final XSLTVersion xsltVersion) {
for (final XSLTVersion appliesToVersion : appliesToVersions) {
@@ -407,7 +420,7 @@ private boolean notApplicableToVersion(final XSLTVersion xsltVersion) {
return true;
}
- public Optional get(final XSLTVersion xsltVersion, final MapType options) throws XPathException {
+ public Optional get(final XSLTVersion xsltVersion, final MapType options) throws XPathException {
if (notApplicableToVersion(xsltVersion)) {
return Optional.empty();
}
@@ -416,6 +429,12 @@ public Optional get(final XSLTVersion xsltVersion, final MapType options) thr
}
}
+ public abstract static class Option extends AbstractOption {
+ private Option(final int itemSubtype, final String name, final Optional defaultValue, final XSLTVersion... appliesToVersions) {
+ super(itemSubtype, new StringValue(name), defaultValue, appliesToVersions);
+ }
+ }
+
static class SequenceOption extends Option {
private final int sequenceSubtype;
@@ -449,6 +468,16 @@ public Optional get(final MapType options) throws XPathException {
}
return defaultValue;
}
+
+ @Override
+ public MapType set(final MapType options, final T value) {
+ return options.put(name, value);
+ }
+
+ @Override
+ public IMap set(final IMap options, final T value) {
+ return options.put(name, value);
+ }
}
static class ItemOption extends Option {
@@ -478,6 +507,60 @@ public Optional get(final MapType options) throws XPathException {
}
return defaultValue;
}
+
+ @Override
+ public MapType set(final MapType options, final T value) {
+ return options.put(name, (Sequence) value);
+ }
+
+ @Override
+ public IMap set(final IMap options, final T value) {
+ return options.put(name, (Sequence) value);
+ }
+ }
+
+ public abstract static class VendorOption extends AbstractOption {
+ private VendorOption(final int itemSubtype, final String name, final Optional defaultValue, final XSLTVersion... appliesToVersions) {
+ super(itemSubtype, new QNameValue(null, new org.exist.dom.QName(name, FeatureKeys.NS)), defaultValue, appliesToVersions);
+ }
+ }
+
+ static class VendorItemOption extends VendorOption {
+ public VendorItemOption(final int itemSubtype, final String name, final XSLTVersion... appliesToVersions) {
+ super(itemSubtype, name, Optional.empty(), appliesToVersions);
+ }
+
+ public VendorItemOption(final int itemSubtype, final String name, @Nullable final T defaultValue, final XSLTVersion... appliesToVersions) {
+ super(itemSubtype, name, Optional.ofNullable(defaultValue), appliesToVersions);
+ }
+
+ @Override
+ public Optional get(final MapType options) throws XPathException {
+ if (options.contains(name)) {
+ final Item item0 = options.get(name).itemAt(0);
+ if (item0 != null) {
+ if (Type.subTypeOf(item0.getType(), itemSubtype)) {
+ return Optional.of((T) item0);
+ } else if (itemSubtype == Type.STRING && Type.subTypeOf(item0.getType(), Type.ANY_ATOMIC_TYPE)) {
+ return Optional.of((T)new StringValue(item0.getStringValue()));
+ } else {
+ throw new XPathException(
+ ErrorCodes.XPTY0004, "Type error: expected " + Type.getTypeName(itemSubtype) + ", got " + Type.getTypeName(item0.getType()));
+ }
+ }
+ }
+ return defaultValue;
+ }
+
+ @Override
+ public MapType set(final MapType options, final T value) {
+ return options.put(name, (Sequence) value);
+ }
+
+ @Override
+ public IMap set(final IMap options, final T value) {
+ return options.put(name, (Sequence) value);
+ }
}
/**
@@ -510,11 +593,11 @@ private Tuple2 getStylesheet(final MapType options) throws XPath
stylesheetText.ifPresent(s -> results.add(Tuple("", new StringSource(s))));
if (results.size() > 1) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "More than one of stylesheet-location, stylesheet-node, and stylesheet-text was set");
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "More than one of stylesheet-location, stylesheet-node, and stylesheet-text was set");
}
if (results.isEmpty()) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "None of stylesheet-location, stylesheet-node, or stylesheet-text was set");
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "None of stylesheet-location, stylesheet-node, or stylesheet-text was set");
}
return results.get(0);
@@ -534,10 +617,10 @@ private Source resolveStylesheetLocation(final String stylesheetLocation) throws
final URI uri = URI.create(stylesheetLocation);
if (uri.isAbsolute()) {
- return URIResolution.resolveDocument(stylesheetLocation, context, fnTransform);
+ return URIResolution.resolveDocument(callingExpression, stylesheetLocation);
} else {
- final AnyURIValue resolved = resolveURI(new AnyURIValue(stylesheetLocation), context.getBaseURI());
- return URIResolution.resolveDocument(resolved.getStringValue(), context, fnTransform);
+ final AnyURIValue resolved = resolveURI(new AnyURIValue(stylesheetLocation), callingExpression.getContext().getBaseURI());
+ return URIResolution.resolveDocument(callingExpression, resolved.getStringValue());
}
}
@@ -552,7 +635,7 @@ private AnyURIValue resolveURI(final AnyURIValue relative, final AnyURIValue bas
try {
return URIResolution.resolveURI(relative, base);
} catch (final URISyntaxException e) {
- throw new XPathException(fnTransform, ErrorCodes.FORG0009, "unable to resolve a relative URI against a base URI in fn:transform(): " + e.getMessage(), null, e);
+ throw new XPathException(callingExpression, ErrorCodes.FORG0009, "unable to resolve a relative URI against a base URI in fn:transform(): " + e.getMessage(), null, e);
}
}
@@ -564,7 +647,7 @@ private XSLTVersion getXsltVersion(final Source xsltStylesheet) throws XPathExce
return staxExtractXsltVersion(xsltStylesheet);
}
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT, unrecognised source");
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Unable to extract version from XSLT, unrecognised source");
}
private XSLTVersion domExtractXsltVersion(final Source xsltStylesheet) throws XPathException {
@@ -597,13 +680,13 @@ private XSLTVersion domExtractXsltVersion(final Source xsltStylesheet) throws XP
}
if (version.isEmpty()) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via DOM");
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via DOM");
}
try {
return XSLTVersion.fromDecimal(new BigDecimal(version));
} catch (final Transform.PendingException pe) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via DOM. Value: " + version + " : " + pe.getMessage());
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via DOM. Value: " + version + " : " + pe.getMessage());
}
}
@@ -627,10 +710,10 @@ private XSLTVersion staxExtractXsltVersion(final Source xsltStylesheet) throws X
}
}
} catch (final XMLStreamException | Transform.PendingException e) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via STaX: " + e.getMessage(), Sequence.EMPTY_SEQUENCE, e);
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via STaX: " + e.getMessage(), Sequence.EMPTY_SEQUENCE, e);
}
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via STaX");
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via STaX");
}
private static class StringSource extends StreamSource {
@@ -651,9 +734,8 @@ static class SystemProperties {
private final RetainedStaticContext retainedStaticContext;
- private SystemProperties(final XQueryContext context) {
- final var saxonConfiguration = context.getBroker().getBrokerPool().getSaxonConfiguration();
- this.retainedStaticContext = new RetainedStaticContext(saxonConfiguration);
+ private SystemProperties(final SaxonConfiguration saxonConfiguration) {
+ this.retainedStaticContext = new RetainedStaticContext(saxonConfiguration.getConfiguration());
}
String get(final org.exist.dom.QName qName) {
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java
index 8ae6be6143..9e5928996f 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java
@@ -53,32 +53,49 @@
import net.sf.saxon.s9api.*;
import net.sf.saxon.serialize.SerializationProperties;
import net.sf.saxon.trans.UncheckedXPathException;
+
+import java.io.IOException;
+import java.io.StringWriter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.exist.dom.INodeHandle;
import org.exist.dom.QName;
+import org.exist.dom.persistent.DocumentImpl;
+import org.exist.dom.persistent.NodeProxy;
+import org.exist.storage.serializers.EXistOutputKeys;
+import org.exist.storage.serializers.NodeValueInputSource;
import org.exist.util.Holder;
import org.exist.xquery.ErrorCodes;
+import org.exist.xquery.Expression;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
-import org.exist.xquery.functions.fn.FnTransform;
import org.exist.xquery.functions.map.MapType;
import org.exist.xquery.value.*;
-import org.w3c.dom.Document;
+import org.exist.xslt.SaxonConfiguration;
+import org.exist.xslt.XsltURIResolverHelper;
import org.w3c.dom.Node;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
+import javax.xml.transform.SourceLocator;
import javax.xml.transform.TransformerException;
-import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.URIResolver;
+import javax.xml.transform.sax.SAXSource;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
import static org.exist.util.StringUtil.isNullOrEmpty;
+import static org.exist.util.StringUtil.notNullOrEmpty;
import static org.exist.xquery.functions.fn.transform.Options.Option.*;
/**
@@ -104,136 +121,184 @@
*/
public class Transform {
- private static final Logger LOGGER = LogManager.getLogger(org.exist.xquery.functions.fn.transform.Transform.class);
+ private static Logger LOGGER = LogManager.getLogger(org.exist.xquery.functions.fn.transform.Transform.class);
private static final org.exist.xquery.functions.fn.transform.Transform.ErrorListenerLog4jAdapter ERROR_LISTENER = new Transform.ErrorListenerLog4jAdapter(Transform.LOGGER);
- final Convert.ToSaxon toSaxon = new Convert.ToSaxon() {
- @Override
- DocumentBuilder newDocumentBuilder() {
- return context.getBroker().getBrokerPool().getSaxonProcessor().newDocumentBuilder();
- }
- };
-
private static final Cache XSLT_EXECUTABLE_CACHE = Caffeine.newBuilder()
.maximumSize(25)
.weakValues()
.build();
- private final XQueryContext context;
- private final FnTransform fnTransform;
+ private final Expression callingExpression;
-
- public Transform(final XQueryContext context, final FnTransform fnTransform) {
- this.context = context;
- this.fnTransform = fnTransform;
+ public Transform(final Expression callingExpression) {
+ this.callingExpression = callingExpression;
}
- public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
+ public MapType eval(final SaxonConfiguration saxonConfiguration, final Convert.ToSaxon toSaxon, final Options options, final Sequence contextSequence, @Nullable final ErrorListener errorListener) throws XPathException {
+ if (!(options.xsltVersion.equals(V1_0) || options.xsltVersion.equals(V2_0) || options.xsltVersion.equals(V3_0))) {
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0001, "xslt-version: " + options.xsltVersion + " is not supported.");
+ }
- final Options options = new Options(context, fnTransform, toSaxon, (MapType) args[0].itemAt(0));
+ try {
+ final Holder compileException = new Holder<>();
+ final XsltExecutable xsltExecutable;
+ if (options.shouldCache.orElse(BooleanValue.TRUE).getValue()) {
+ xsltExecutable = Transform.XSLT_EXECUTABLE_CACHE.get(executableCacheKey(options), key -> {
+ try {
+ return compileExecutable(saxonConfiguration, toSaxon, options);
+ } catch (final XPathException e) {
+ compileException.value = e;
+ return null;
+ }
+ });
+ } else {
+ xsltExecutable = compileExecutable(saxonConfiguration, toSaxon, options);
+ }
- //TODO(AR) Saxon recommends to use a StreamSource or SAXSource instead of DOMSource for performance
- final Optional sourceNode = Transform.getSourceNode(options.sourceNode, context.getBaseURI());
+ if (compileException.value != null) {
+ // if we could not compile the xslt, rethrow the error
+ throw compileException.value;
+ }
+ if (xsltExecutable == null) {
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0003, "Unable to compile stylesheet (No error returned from compilation)");
+ }
- if (options.xsltVersion.equals(V1_0) || options.xsltVersion.equals(V2_0) || options.xsltVersion.equals(V3_0)) {
- try {
- final Holder compileException = new Holder<>();
- final XsltExecutable xsltExecutable;
- if (options.shouldCache.orElse(BooleanValue.TRUE).getValue()) {
- xsltExecutable = Transform.XSLT_EXECUTABLE_CACHE.get(executableHash(options), key -> {
- try {
- return compileExecutable(options);
- } catch (final XPathException e) {
- compileException.value = e;
- return null;
- }
- });
- } else {
- xsltExecutable = compileExecutable(options);
+ final Xslt30Transformer xslt30Transformer = xsltExecutable.load30();
+ xslt30Transformer.setMessageListener(new XsltMessageListener(saxonConfiguration.getProcessor(), getLogger()));
+ @Nullable final String base = options.resolvedStylesheetBaseURI.map(AnyURIValue::getStringValue).orElse(null);
+ final URIResolver uriResolver = XsltURIResolverHelper.getXsltURIResolver(callingExpression.getContext().getBroker().getBrokerPool(), xslt30Transformer.getURIResolver(), base, true);
+ xslt30Transformer.setURIResolver(uriResolver);
+
+ options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName())));
+ xslt30Transformer.setInitialTemplateParameters(options.templateParams, false);
+ xslt30Transformer.setInitialTemplateParameters(options.tunnelParams, true);
+ if (errorListener != null) {
+ xslt30Transformer.setErrorListener(errorListener);
+ }
+ if (options.baseOutputURI.isPresent()) {
+ final AtomicValue baseOutputURI = options.baseOutputURI.get();
+ final AtomicValue asString = baseOutputURI.convertTo(Type.STRING);
+ if (asString instanceof StringValue) {
+ xslt30Transformer.setBaseOutputURI(asString.getStringValue());
}
+ }
- if (compileException.value != null) {
- // if we could not compile the xslt, rethrow the error
- throw compileException.value;
- }
- if (xsltExecutable == null) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0003, "Unable to compile stylesheet (No error returned from compilation)");
- }
+ // The delivery mechanism
+ final SerializationProperties serializationProperties =
+ SerializationParameters.getAsSerializationProperties(
+ options.serializationParams.orElse(new MapType(callingExpression.getContext())),
+ (code, message) -> new XPathException(callingExpression, code, message));
+ final Delivery delivery = new Delivery(callingExpression.getContext(), options.deliveryFormat, serializationProperties);
+
+ // Record the secondary result documents generated
+ final Map resultDocuments = new HashMap<>();
+ xslt30Transformer.setResultDocumentHandler(resultDocumentURI -> {
+ final Delivery resultDelivery = new Delivery(callingExpression.getContext(), options.deliveryFormat, serializationProperties);
+ resultDocuments.put(resultDocumentURI, resultDelivery);
+ return resultDelivery.createDestination(xslt30Transformer, true);
+ });
+
+ if (options.globalContextItem.isPresent()) {
+ final Item globalContextItem = options.globalContextItem.get();
+ if (globalContextItem instanceof NodeValue) {
+ final NodeValue globalContextItemNodeValue = (NodeValue) globalContextItem;
+ final boolean globalContextItemIsDocument = ((INodeHandle>) globalContextItemNodeValue).getNodeType() == Node.DOCUMENT_NODE;
+
+ // read the global-context-item from Elemental and transform it with Saxon
+ final InputSource globalContextItemInputSource = new NodeValueInputSource(globalContextItemNodeValue, getBaseURI(globalContextItemNodeValue, callingExpression));
+ final org.exist.storage.serializers.Serializer xmlReader = callingExpression.getContext().getBroker().borrowSerializer();
+ try {
+ configureXmlReader(xmlReader, options);
+ final Source source = new SAXSource(xmlReader, globalContextItemInputSource);
+ final DocumentBuilder globalContextItemBuilder = toSaxon.newDocumentBuilder();
+ XdmNode xdmNode = globalContextItemBuilder.build(source);
+ // TODO(AR) START TEMP
+ if (!globalContextItemIsDocument) {
+ final Iterator children = xdmNode.children().iterator();
+ if (children.hasNext()) {
+ xdmNode = children.next();
+ }
+ }
+ // TODO(AR) END TEMP
+ xslt30Transformer.setGlobalContextItem(xdmNode);
- final Xslt30Transformer xslt30Transformer = xsltExecutable.load30();
+ } finally {
+ callingExpression.getContext().getBroker().returnSerializer(xmlReader);
+ }
+ } else {
+ final XdmItem xdmItem = (XdmItem) toSaxon.of(globalContextItem);
+ xslt30Transformer.setGlobalContextItem(xdmItem);
+ }
- options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName())));
- xslt30Transformer.setInitialTemplateParameters(options.templateParams, false);
- xslt30Transformer.setInitialTemplateParameters(options.tunnelParams, true);
- if (options.baseOutputURI.isPresent()) {
- final AtomicValue baseOutputURI = options.baseOutputURI.get();
- final AtomicValue asString = baseOutputURI.convertTo(Type.STRING);
- if (asString instanceof StringValue) {
- xslt30Transformer.setBaseOutputURI(asString.getStringValue());
+ } else if (options.sourceNode.isPresent()) {
+ // set the global-context-item as the root of the tree containing the source node
+ NodeValue globalContextItemNodeValue = options.sourceNode.get();
+
+ if (((INodeHandle>) globalContextItemNodeValue).getNodeType() != Node.DOCUMENT_NODE) {
+ // global-context-item is not at the root of the tree, so set it to the root
+ if (globalContextItemNodeValue.getImplementationType() == NodeValue.PERSISTENT_NODE) {
+ // Persistent DOM
+ globalContextItemNodeValue = NodeProxy.wrap(callingExpression, (DocumentImpl) globalContextItemNodeValue.getOwnerDocument());
+ } else {
+ // In-Memory DOM
+ globalContextItemNodeValue = (org.exist.dom.memtree.DocumentImpl) globalContextItemNodeValue.getOwnerDocument();
}
}
- // The delivery mechanism
- final SerializationProperties serializationProperties =
- SerializationParameters.getAsSerializationProperties(
- options.serializationParams.orElse(new MapType(context)),
- (code, message) -> new XPathException(fnTransform, code, message));
- final Delivery delivery = new Delivery(context, options.deliveryFormat, serializationProperties);
-
- // Record the secondary result documents generated
- final Map resultDocuments = new HashMap<>();
- xslt30Transformer.setResultDocumentHandler(resultDocumentURI -> {
- final Delivery resultDelivery = new Delivery(context, options.deliveryFormat, serializationProperties);
- resultDocuments.put(resultDocumentURI, resultDelivery);
- return resultDelivery.createDestination(xslt30Transformer, true);
- });
+ // read the global-context-item from Elemental and set it in Saxon
+ final InputSource globalContextItemInputSource = new NodeValueInputSource(globalContextItemNodeValue, getBaseURI(globalContextItemNodeValue, callingExpression));
- if (options.globalContextItem.isPresent()) {
- final Item item = options.globalContextItem.get();
- final XdmItem xdmItem = (XdmItem) toSaxon.of(item);
- xslt30Transformer.setGlobalContextItem(xdmItem);
- } else if (sourceNode.isPresent()) {
- final Document document;
- Source source = sourceNode.get();
- final Node node = ((DOMSource)sourceNode.get()).getNode();
- if (!(node instanceof org.exist.dom.memtree.DocumentImpl) && !(node instanceof org.exist.dom.persistent.DocumentImpl)) {
- //The source may not be a document
- //If it isn't, it should be part of a document, so we build a DOMSource to use
- document = node.getOwnerDocument();
- source = new DOMSource(document);
- }
- final var brokerPool = context.getBroker().getBrokerPool();
- final DocumentBuilder sourceBuilder = brokerPool.getSaxonProcessor().newDocumentBuilder();
+ final org.exist.storage.serializers.Serializer xmlReader = callingExpression.getContext().getBroker().borrowSerializer();
+ try {
+ configureXmlReader(xmlReader, options);
+ final Source source = new SAXSource(xmlReader, globalContextItemInputSource);
+ final DocumentBuilder sourceBuilder = toSaxon.newDocumentBuilder();
final XdmNode xdmNode = sourceBuilder.build(source);
xslt30Transformer.setGlobalContextItem(xdmNode);
- } else {
- xslt30Transformer.setGlobalContextItem(null);
- }
- final Transform.TemplateInvocation invocation = new Transform.TemplateInvocation(
- options, sourceNode, delivery, xslt30Transformer, resultDocuments);
- return invocation.invoke();
- } catch (final SaxonApiException e) {
- throw originalXPathException("Could not transform input using: " + options.xsltSource._1 + ", at line: " + e.getLineNumber() + ". Error: ", e, ErrorCodes.FOXT0003);
- } catch (final UncheckedXPathException e) {
- final Location location = e.getXPathException().getLocator();
- int line = -1;
- int column = -1;
- if (location != null) {
- line = location.getLineNumber();
- column = location.getColumnNumber();
+ //TODO(AR) remove this after testing
+// DOMSource source = (DOMSource) sourceNode.get();
+// Node node = source.getNode();
+// if (node.getNodeType() != Node.DOCUMENT_NODE) {
+// // not at the root of the tree, so get the root
+// node = node.getOwnerDocument();
+// source = new DOMSource(node, node.getBaseURI());
+// }
+
+// final DocumentBuilder sourceBuilder = toSaxon.newDocumentBuilder();
+// final XdmNode xdmNode = sourceBuilder.build(source);
+// xslt30Transformer.setGlobalContextItem(xdmNode);
+
+ } finally {
+ callingExpression.getContext().getBroker().returnSerializer(xmlReader);
}
- throw originalXPathException("Could not transform input using: " + options.xsltSource._1 + ", at line: " + line + ", column: " + column + ". Error: ", e, ErrorCodes.FOXT0003);
+
+ } else {
+ xslt30Transformer.setGlobalContextItem(null);
}
- } else {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0001, "xslt-version: " + options.xsltVersion + " is not supported.");
+ final Transform.TemplateInvocation invocation = new Transform.TemplateInvocation(
+ options, delivery, xslt30Transformer, resultDocuments);
+ return invocation.invoke(toSaxon);
+
+ } catch (final SaxonApiException e) {
+ throw originalXPathException("Could not transform input using: " + options.xsltSource._1 + ", at line: " + e.getLineNumber() + ". Error: ", e, ErrorCodes.FOXT0003);
+ } catch (final UncheckedXPathException e) {
+ final Location location = e.getXPathException().getLocator();
+ int line = -1;
+ int column = -1;
+ if (location != null) {
+ line = location.getLineNumber();
+ column = location.getColumnNumber();
+ }
+ throw originalXPathException("Could not transform input using: " + options.xsltSource._1 + ", at line: " + line + ", column: " + column + ". Error: ", e, ErrorCodes.FOXT0003);
}
}
- private XsltExecutable compileExecutable(final Options options) throws XPathException {
- final XsltCompiler xsltCompiler = context.getBroker().getBrokerPool().getSaxonProcessor().newXsltCompiler();
+ private XsltExecutable compileExecutable(final SaxonConfiguration saxonConfiguration, final Convert.ToSaxon toSaxon, final Options options) throws XPathException {
+ final XsltCompiler xsltCompiler = saxonConfiguration.getProcessor().newXsltCompiler();
final SingleRequestErrorListener errorListener = new SingleRequestErrorListener(Transform.ERROR_LISTENER);
xsltCompiler.setErrorListener(errorListener);
@@ -247,25 +312,28 @@ private XsltExecutable compileExecutable(final Options options) throws XPathExce
xsltCompiler.setParameter(new net.sf.saxon.s9api.QName(qKey.getPrefix(), qKey.getLocalPart()), value);
}
- xsltCompiler.setURIResolver(new URIResolution.CompileTimeURIResolver(context, fnTransform) {
- @Override public Source resolve(final String href, final String base) throws TransformerException {
- // Correct error from URI resolution when there is no base
- try {
- final URI hrefURI = URI.create(href);
- if (options.resolvedStylesheetBaseURI.isEmpty() && !hrefURI.isAbsolute() && isNullOrEmpty(base)) {
- final XPathException resolutionException = new XPathException(fnTransform,
- ErrorCodes.XTSE0165,
- "transform using a relative href, \n" +
- "using option stylesheet-text, but without stylesheet-base-uri");
- throw new TransformerException(resolutionException);
- }
- } catch (final IllegalArgumentException e) {
- throw new TransformerException(e);
- }
- // Checked the special error case, defer to eXist resolution
- return super.resolve(href, base);
- }
- });
+ @Nullable final String base = options.resolvedStylesheetBaseURI.map(AnyURIValue::getStringValue).orElse(null);
+ final URIResolver uriResolver = XsltURIResolverHelper.getXsltURIResolver(callingExpression.getContext().getBroker().getBrokerPool(), xsltCompiler.getURIResolver(), base, true);
+ xsltCompiler.setURIResolver(uriResolver);
+// xsltCompiler.setURIResolver(new URIResolution.CompileTimeURIResolver(callingExpression) {
+// @Override public Source resolve(final String href, final String base) throws TransformerException {
+// // Correct error from URI resolution when there is no base
+// try {
+// final URI hrefURI = URI.create(href);
+// if (options.resolvedStylesheetBaseURI.isEmpty() && !hrefURI.isAbsolute() && isNullOrEmpty(base)) {
+// final XPathException resolutionException = new XPathException(callingExpression,
+// ErrorCodes.XTSE0165,
+// "transform using a relative href, \n" +
+// "using option stylesheet-text, but without stylesheet-base-uri");
+// throw new TransformerException(resolutionException);
+// }
+// } catch (final IllegalArgumentException e) {
+// throw new TransformerException(e);
+// }
+// // Checked the special error case, defer to eXist resolution
+// return super.resolve(href, base);
+// }
+// });
try {
options.resolvedStylesheetBaseURI.ifPresent(anyURIValue -> options.xsltSource._2.setSystemId(anyURIValue.getStringValue()));
@@ -289,7 +357,7 @@ private XPathException originalXPathException(final String prefix, @Nonnull fina
Throwable cause = e;
while (cause != null) {
if (cause instanceof XPathException) {
- return new XPathException(fnTransform, ((XPathException) cause).getErrorCode(), prefix + cause.getMessage());
+ return new XPathException(callingExpression, ((XPathException) cause).getErrorCode(), prefix + cause.getMessage(), cause);
}
cause = cause.getCause();
}
@@ -306,15 +374,15 @@ private XPathException originalXPathException(final String prefix, @Nonnull fina
} catch (final IllegalArgumentException ee) {
errorCode = new ErrorCodes.DynamicErrorCode(errorCodeQName, cause.getMessage());
}
- return new XPathException(fnTransform, errorCode, prefix + cause.getMessage());
+ return new XPathException(callingExpression, errorCode, prefix + cause.getMessage());
} else {
- return new XPathException(fnTransform, defaultErrorCode, prefix + cause.getMessage());
+ return new XPathException(callingExpression, defaultErrorCode, prefix + cause.getMessage());
}
}
cause = cause.getCause();
}
- return new XPathException(fnTransform, defaultErrorCode, prefix + e.getMessage());
+ return new XPathException(callingExpression, defaultErrorCode, prefix + e.getMessage());
}
/**
@@ -324,8 +392,8 @@ private XPathException originalXPathException(final String prefix, @Nonnull fina
* @param options options to read
* @return a string, the hash we want
*/
- private String executableHash(final Options options) {
-
+ private String executableCacheKey(final Options options) {
+ // TODO(AR) this needs improving should use a dedicated class ExecutableCacheKey for the return value - that class should contain only members that need to be compared for equality to determine the key
final String uniquifier;
if (options.resolvedStylesheetBaseURI.isPresent() || options.sourceTextChecksum.isPresent()) {
uniquifier = "";
@@ -343,35 +411,72 @@ private String executableHash(final Options options) {
options.stylesheetNodeDocumentPath,
options.stylesheetNodeDocumentPath).toString();
- return Tuple(locationHash, paramHash).toString();
+ return Tuple(options.saxonConfiguration.getConfiguration().hashCode(), locationHash, paramHash).toString();
+ }
+
+ private static void configureXmlReader(final org.exist.storage.serializers.Serializer xmlReader, final Options options) throws XPathException {
+ if (options.vendorOptions.isPresent()) {
+ final MapType vendorOptions = options.vendorOptions.get();
+ final boolean expandXincludes = Options.EXPAND_XINCLUDES.get(options.xsltVersion, vendorOptions).map(BooleanValue::getValue).orElse(false);
+ try {
+ xmlReader.setProperty(EXistOutputKeys.EXPAND_XINCLUDES, expandXincludes ? "yes" : "no");
+ } catch (final SAXNotRecognizedException | SAXNotSupportedException e) {
+ // no-op - exception will never be thrown in practice
+ }
+ if (expandXincludes) {
+ @Nullable final String xincludePath = Options.XINCLUDE_PATH.get(options.xsltVersion, vendorOptions).map(StringValue::getStringValue).orElse(null);
+ if (xincludePath != null) {
+ xmlReader.getXIncludeFilter().setModuleLoadPath(xincludePath);
+ }
+ }
+ }
+ }
+
+ private static @Nullable String getBaseURI(final NodeValue nodeValue, final Expression callingExpression) {
+ @Nullable String baseUri = nodeValue.getNode().getBaseURI();
+ if (baseUri == null) {
+ try {
+ final AnyURIValue contextBaseURI = callingExpression.getContext().getBaseURI();
+ if (contextBaseURI != null) {
+ baseUri = contextBaseURI.getStringValue();
+ }
+ } catch (final XPathException e) {
+ // ignore
+ return null;
+ }
+ }
+
+ if (isNullOrEmpty(baseUri)) {
+ return null;
+ }
+
+ return baseUri;
}
private class TemplateInvocation {
final Options options;
- Optional sourceNode;
final Delivery delivery;
final Destination destination;
final Xslt30Transformer xslt30Transformer;
final Map resultDocuments;
- TemplateInvocation(final Options options, final Optional sourceNode, final Delivery delivery, final Xslt30Transformer xslt30Transformer, final Map resultDocuments) {
+ TemplateInvocation(final Options options, final Delivery delivery, final Xslt30Transformer xslt30Transformer, final Map resultDocuments) {
this.options = options;
- this.sourceNode = sourceNode;
this.delivery = delivery;
this.destination = delivery.createDestination(xslt30Transformer, false);
this.xslt30Transformer = xslt30Transformer;
this.resultDocuments = resultDocuments;
}
- private MapType invokeCallFunction() throws XPathException, SaxonApiException {
+ private MapType invokeCallFunction(final Convert.ToSaxon toSaxon) throws XPathException, SaxonApiException {
assert options.initialFunction.isPresent();
final net.sf.saxon.s9api.QName qName = Convert.ToSaxon.of(options.initialFunction.get().getQName());
final XdmValue[] functionParams;
if (options.functionParams.isPresent()) {
functionParams = toSaxon.of(options.functionParams.get());
} else {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Error - transform using XSLT 3.0 option initial-function, but the corresponding option function-params was not supplied.");
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002, "Error - transform using XSLT 3.0 option initial-function, but the corresponding option function-params was not supplied.");
}
xslt30Transformer.callFunction(qName, functionParams, destination);
@@ -381,7 +486,7 @@ private MapType invokeCallFunction() throws XPathException, SaxonApiException {
private MapType invokeCallTemplate() throws XPathException, SaxonApiException {
assert options.initialTemplate.isPresent();
if (options.initialMode.isPresent()) {
- throw new XPathException(fnTransform, ErrorCodes.FOXT0002,
+ throw new XPathException(callingExpression, ErrorCodes.FOXT0002,
Options.INITIAL_MODE.name + " supplied indicating apply-templates invocation, " +
"AND " + Options.INITIAL_TEMPLATE.name + " supplied indicating call-template invocation.");
}
@@ -396,21 +501,61 @@ private MapType invokeCallTemplate() throws XPathException, SaxonApiException {
return makeResultMap(options, delivery, resultDocuments);
}
- private MapType invokeApplyTemplates() throws XPathException, SaxonApiException {
+ private MapType invokeApplyTemplates(final Convert.ToSaxon toSaxon) throws XPathException, SaxonApiException {
if (options.initialMatchSelection.isPresent()) {
final Sequence initialMatchSelection = options.initialMatchSelection.get();
- final Item item = initialMatchSelection.itemAt(0);
- if (item instanceof Document) {
- final Source sourceIMS = new DOMSource((Document)item, context.getBaseURI().getStringValue());
- xslt30Transformer.applyTemplates(sourceIMS, destination);
+ final Item initialMatchSelectionItem = initialMatchSelection.itemAt(0);
+ if (initialMatchSelectionItem instanceof NodeValue) {
+
+ final NodeValue initialMatchSelectionNodeValue = (NodeValue) initialMatchSelectionItem;
+
+ // read the initial match selection from Elemental and transform it with Saxon
+ final InputSource initialMatchSelectionInputSource = new NodeValueInputSource(initialMatchSelectionNodeValue, getBaseURI(initialMatchSelectionNodeValue, callingExpression));
+ final org.exist.storage.serializers.Serializer xmlReader = callingExpression.getContext().getBroker().borrowSerializer();
+ try {
+ configureXmlReader(xmlReader, options);
+ final Source source = new SAXSource(xmlReader, initialMatchSelectionInputSource);
+ xslt30Transformer.applyTemplates(source, destination);
+
+ } finally {
+ callingExpression.getContext().getBroker().returnSerializer(xmlReader);
+ }
+
+ // TODO (AR) remove this after testing
+// final Source sourceIMS = new DOMSource((Document)item, callingExpression.getContext().getBaseURI().getStringValue());
+// xslt30Transformer.applyTemplates(sourceIMS, destination);
} else {
final XdmValue selection = toSaxon.of(initialMatchSelection);
xslt30Transformer.applyTemplates(selection, destination);
}
- } else if (sourceNode.isPresent()) {
- xslt30Transformer.applyTemplates(sourceNode.get(), destination);
+ } else if (options.sourceNode.isPresent()) {
+ final NodeValue sourceNode = options.sourceNode.get();
+ final boolean sourceNodeIsDocument = ((INodeHandle>) sourceNode).getNodeType() == Node.DOCUMENT_NODE;
+
+ // read the source node from Elemental and transform it with Saxon
+ final InputSource sourceNodeInputSource = new NodeValueInputSource(sourceNode, getBaseURI(sourceNode, callingExpression));
+ final org.exist.storage.serializers.Serializer xmlReader = callingExpression.getContext().getBroker().borrowSerializer();
+ try {
+ configureXmlReader(xmlReader, options);
+
+ // TODO(AR) START TEMP
+ if (!sourceNodeIsDocument) {
+ try {
+ xmlReader.setProperty(org.exist.storage.serializers.Serializer.GENERATE_DOC_EVENTS, "false");
+ } catch (final SAXNotRecognizedException | SAXNotSupportedException e) {
+ // no-op
+ }
+ }
+ // TODO(AR) END TEMP
+
+ final Source source = new SAXSource(xmlReader, sourceNodeInputSource);
+ xslt30Transformer.applyTemplates(source, destination);
+
+ } finally {
+ callingExpression.getContext().getBroker().returnSerializer(xmlReader);
+ }
} else {
- throw new XPathException(fnTransform,
+ throw new XPathException(callingExpression,
ErrorCodes.FOXT0002,
"One of " + Options.SOURCE_NODE.name + " or " +
Options.INITIAL_MATCH_SELECTION.name + " or " +
@@ -420,19 +565,19 @@ private MapType invokeApplyTemplates() throws XPathException, SaxonApiException
return makeResultMap(options, delivery, resultDocuments);
}
- private MapType invoke() throws XPathException, SaxonApiException {
+ MapType invoke(final Convert.ToSaxon toSaxon) throws XPathException, SaxonApiException {
if (options.initialFunction.isPresent()) {
- return invokeCallFunction();
+ return invokeCallFunction(toSaxon);
} else if (options.initialTemplate.isPresent()) {
return invokeCallTemplate();
} else {
- return invokeApplyTemplates();
+ return invokeApplyTemplates(toSaxon);
}
}
private MapType makeResultMap(final Options options, final Delivery primaryDelivery, final Map resultDocuments) throws XPathException {
- try (final MapType outputMap = new MapType(context)) {
+ try (final MapType outputMap = new MapType(callingExpression.getContext())) {
final AtomicValue outputKey;
outputKey = options.baseOutputURI.orElseGet(() -> new StringValue("output"));
@@ -460,8 +605,17 @@ private Sequence postProcess(final AtomicValue key, final Sequence before, final
}
}
- private static Optional getSourceNode(final Optional sourceNode, final AnyURIValue baseURI) {
- return sourceNode.map(NodeValue::getNode).map(node -> new DOMSource(node, baseURI.getStringValue()));
+ /**
+ * Designed to be package-protected accessible so that we can observe logging in tests.
+ *
+ * @param logger the logger to use in testing.
+ */
+ static void setLogger(final Logger logger) {
+ LOGGER = logger;
+ }
+
+ private Logger getLogger() {
+ return LOGGER;
}
private static class ErrorListenerLog4jAdapter implements ErrorListener {
@@ -532,4 +686,55 @@ public PendingException(String message, Throwable cause) {
super(message, cause);
}
}
+
+ private static class XsltMessageListener implements MessageListener {
+
+ private final Processor processor;
+ private final Logger logger;
+
+ public XsltMessageListener(final Processor processor, final Logger logger) {
+ this.processor = processor;
+ this.logger = logger;
+ }
+
+ @Override
+ public void message(final XdmNode content, final boolean terminate, final SourceLocator locator) {
+
+ try (final StringWriter writer = new StringWriter()) {
+ final Serializer serializer = processor.newSerializer();
+ serializer.setOutputProperty(Serializer.Property.OMIT_XML_DECLARATION, "yes");
+ serializer.setOutputWriter(writer);
+ serializer.serializeNode(content);
+
+ @Nullable final String source;
+ final int sourceLine;
+ final int sourceColumn;
+ if (locator != null) {
+ source = locator.getSystemId();
+ sourceLine = locator.getLineNumber();
+ sourceColumn = locator.getColumnNumber();
+ } else {
+ source = null;
+ sourceLine = -1;
+ sourceColumn = -1;
+ }
+
+ final StringBuilder tag = new StringBuilder("");
+
+ logger.info("{}{}", tag.toString(), writer.toString());
+ } catch (final SaxonApiException e) {
+ logger.error("Unable to serialize xsl:message content", e);
+ } catch (final IOException e) {
+ logger.error("Unable to close xsl:message writer", e);
+ }
+ }
+ }
}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java
index 5abbe73da1..6a7facdcb9 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -19,7 +43,6 @@
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-
package org.exist.xquery.functions.fn.transform;
import org.exist.dom.persistent.NodeProxy;
@@ -28,7 +51,6 @@
import org.exist.xquery.ErrorCodes;
import org.exist.xquery.Expression;
import org.exist.xquery.XPathException;
-import org.exist.xquery.XQueryContext;
import org.exist.xquery.util.DocUtils;
import org.exist.xquery.value.AnyURIValue;
import org.exist.xquery.value.Sequence;
@@ -42,7 +64,7 @@
import java.net.URI;
import java.net.URISyntaxException;
-public class URIResolution {
+class URIResolution {
/**
* URI resolution, the core should be the same as for fn:resolve-uri
@@ -70,13 +92,10 @@ static AnyURIValue resolveURI(final AnyURIValue relative, final AnyURIValue base
}
public static class CompileTimeURIResolver implements URIResolver {
+ private final Expression callingExpression;
- private final XQueryContext xQueryContext;
- private final Expression containingExpression;
-
- public CompileTimeURIResolver(XQueryContext xQueryContext, Expression containingExpression) {
- this.xQueryContext = xQueryContext;
- this.containingExpression = containingExpression;
+ public CompileTimeURIResolver(final Expression callingExpression) {
+ this.callingExpression = callingExpression;
}
@Override
@@ -97,39 +116,43 @@ public Source resolve(final String href, final String base) throws TransformerEx
}
protected Source resolveDocument(final String location) throws XPathException {
- return URIResolution.resolveDocument(location, xQueryContext, containingExpression);
+ return URIResolution.resolveDocument(callingExpression, location);
}
}
/**
* Resolve an absolute document location, stylesheet or included source
*
- * @param location of the stylesheet
- * @return the resolved stylesheet as a source
+ * @param callingExpression the calling expression.
+ * @param location of the stylesheet.
+ *
+ * @return the resolved stylesheet as a source.
+ *
* @throws org.exist.xquery.XPathException if the item does not exist, or is not a document
*/
- static Source resolveDocument(final String location, final XQueryContext xQueryContext, Expression containingExpression) throws XPathException {
+ static Source resolveDocument(final Expression callingExpression, final String location) throws XPathException {
final Sequence document;
try {
- document = DocUtils.getDocument(xQueryContext, location);
+ document = DocUtils.getDocument(callingExpression.getContext(), location);
} catch (final PermissionDeniedException e) {
- throw new XPathException(containingExpression, ErrorCodes.FODC0002,
+ throw new XPathException(callingExpression, ErrorCodes.FODC0002,
"Can not access '" + location + "'" + e.getMessage());
}
if (document == null || document.isEmpty()) {
- throw new XPathException(containingExpression, ErrorCodes.FODC0002,
+ throw new XPathException(callingExpression, ErrorCodes.FODC0002,
"No document found at location '"+ location);
}
if (document.hasOne() && Type.subTypeOf(document.getItemType(), Type.NODE)) {
if (document instanceof NodeProxy proxy) {
- return new DOMSource(proxy.getNode());
+ final Node node = proxy.getNode();
+ return new DOMSource(node, node.getBaseURI());
}
else if (document.itemAt(0) instanceof Node node) {
- return new DOMSource(node);
+ return new DOMSource(node, node.getBaseURI());
}
}
- throw new XPathException(containingExpression, ErrorCodes.FODC0002,
+ throw new XPathException(callingExpression, ErrorCodes.FODC0002,
"Location '"+ location + "' returns an item which is not a document node");
}
}
\ No newline at end of file
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java b/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java
index 4a72e064aa..3ecd259564 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java
@@ -262,7 +262,7 @@ public Sequence get(AtomicValue key) {
}
@Override
- public AbstractMapType put(final AtomicValue key, final Sequence value) {
+ public MapType put(final AtomicValue key, final Sequence value) {
final IMap newMap = map.put(key, value);
return new MapType(getExpression(), this.context, newMap, keyType == key.getType() ? keyType : MIXED_KEY_TYPES);
}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/transform/Transform.java b/exist-core/src/main/java/org/exist/xquery/functions/transform/Transform.java
index f135703f66..7aac864e7e 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/transform/Transform.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/transform/Transform.java
@@ -17,478 +17,385 @@
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
- * The original license header is included below.
- *
- * =====================================================================
- *
- * eXist-db Open Source Native XML Database
- * Copyright (C) 2001 The eXist-db Authors
- *
- * info@exist-db.org
- * http://www.exist-db.org
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.exist.xquery.functions.transform;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import com.evolvedbinary.j8fu.Either;
+import com.evolvedbinary.j8fu.tuple.Tuple2;
+import io.lacuna.bifurcan.IMap;
+import io.lacuna.bifurcan.Map;
+import org.exist.dom.INodeHandle;
import org.exist.dom.QName;
import org.exist.dom.memtree.DocumentBuilderReceiver;
-import org.exist.dom.memtree.MemTreeBuilder;
import org.exist.dom.persistent.NodeProxy;
import org.exist.http.servlets.ResponseWrapper;
-import org.exist.numbering.NodeId;
import org.exist.storage.serializers.EXistOutputKeys;
-import org.exist.storage.serializers.Serializer;
-import org.exist.storage.serializers.XIncludeFilter;
-import org.exist.util.serializer.Receiver;
-import org.exist.util.serializer.ReceiverToSAX;
+import org.exist.storage.serializers.FeatureKeys;
+import org.exist.util.serializer.XQuerySerializer;
import org.exist.xmldb.XmldbURI;
-import org.exist.xquery.*;
-import org.exist.xquery.value.*;
-import org.exist.xslt.Stylesheet;
-import org.exist.xslt.TemplatesFactory;
-import org.exist.xslt.TransformerFactoryAllocator;
+import org.exist.xquery.value.BooleanValue;
+import org.exist.xslt.SaxonConfiguration;
+import org.exist.xquery.BasicFunction;
+import org.exist.xquery.ErrorCodes;
+import org.exist.xquery.Expression;
+import org.exist.xquery.FunctionSignature;
+import org.exist.xquery.Option;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQueryContext;
+import org.exist.xquery.functions.fn.transform.Convert;
+import org.exist.xquery.functions.fn.transform.Options;
+import org.exist.xquery.functions.map.MapType;
+import org.exist.xquery.value.AnyURIValue;
+import org.exist.xquery.value.AtomicValue;
+import org.exist.xquery.value.FunctionParameterSequenceType;
+import org.exist.xquery.value.Item;
+import org.exist.xquery.value.NodeValue;
+import org.exist.xquery.value.QNameValue;
+import org.exist.xquery.value.Sequence;
+import org.exist.xquery.value.SequenceIterator;
+import org.exist.xquery.value.StringValue;
+import org.exist.xquery.value.Type;
+import org.exist.xquery.value.ValueSequence;
import org.exist.xslt.XSLTErrorsListener;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
-import javax.xml.transform.*;
-import javax.xml.transform.sax.SAXResult;
-import javax.xml.transform.sax.TransformerHandler;
-import javax.xml.transform.stream.StreamResult;
-import java.io.BufferedOutputStream;
+import javax.annotation.Nullable;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.TransformerException;
import java.io.IOException;
-import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Properties;
+import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
+import static org.exist.xquery.FunctionDSL.arities;
+import static org.exist.xquery.FunctionDSL.arity;
+import static org.exist.xquery.FunctionDSL.optManyParam;
+import static org.exist.xquery.FunctionDSL.optParam;
+import static org.exist.xquery.FunctionDSL.param;
+import static org.exist.xquery.FunctionDSL.returnsNothing;
+import static org.exist.xquery.FunctionDSL.returnsOptMany;
+import static org.exist.xquery.functions.transform.TransformModule.functionSignatures;
+
/**
- * @author Wolfgang Meier
+ * @author Adam Retter
*/
public class Transform extends BasicFunction {
- public final static FunctionSignature[] signatures = {
- new FunctionSignature(
- new QName("transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX),
- "Applies an XSL stylesheet to the node tree passed as first argument. The stylesheet " +
- "is specified in the second argument. This should either be an URI or a node. If it is an " +
- "URI, it can either point to an external location or to an XSL stored in the db by using the " +
- "'xmldb:' scheme. Stylesheets are cached unless they were just created from an XML " +
- "fragment and not from a complete document. " +
- "Stylesheet parameters " +
- "may be passed in the third argument using an XML fragment with the following structure: " +
- "" +
- ". There are two special parameters named \"exist:stop-on-warn\" and " +
- "\"exist:stop-on-error\". If set to value \"yes\", eXist will generate an XQuery error " +
- "if the XSL processor reports a warning or error.",
- new SequenceType[]{
- new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"),
- new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"),
- new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters")
- },
- new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_ONE, "the transformed result (node tree)")),
- new FunctionSignature(
- new QName("transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX),
- "Applies an XSL stylesheet to the node tree passed as first argument. The stylesheet " +
- "is specified in the second argument. This should either be an URI or a node. If it is an " +
- "URI, it can either point to an external location or to an XSL stored in the db by using the " +
- "'xmldb:' scheme. Stylesheets are cached unless they were just created from an XML " +
- "fragment and not from a complete document. " +
- "Stylesheet parameters " +
- "may be passed in the third argument using an XML fragment with the following structure: " +
- "" +
- ". There are two special parameters named \"exist:stop-on-warn\" and " +
- "\"exist:stop-on-error\". If set to value \"yes\", eXist will generate an XQuery error " +
- "if the XSL processor reports a warning or error. " +
- "The fourth argument specifies attributes to be set on the used Java TransformerFactory with the following structure: " +
- ". " +
- "The fifth argument specifies serialization " +
- "options in the same way as if they " +
- "were passed to \"declare option exist:serialize\" expression. An additional serialization option, " +
- "\"xinclude-path\", is supported, which specifies a base path against which xincludes will be expanded " +
- "(if there are xincludes in the document). A relative path will be relative to the current " +
- "module load path.",
- new SequenceType[]{
- new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"),
- new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"),
- new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters"),
- new FunctionParameterSequenceType("attributes", Type.NODE, Cardinality.ZERO_OR_ONE, "Attributes to pass to the transformation factory"),
- new FunctionParameterSequenceType("serialization-options", Type.STRING, Cardinality.ZERO_OR_ONE, "The serialization options")},
- new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_ONE, "the transformed result (node tree)")),
- new FunctionSignature(
- new QName("stream-transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX),
- "Applies an XSL stylesheet to the node tree passed as first argument. The parameters are the same " +
- "as for the transform function. stream-transform can only be used within a servlet context. Instead " +
- "of returning the transformed document fragment, it directly streams its output to the servlet's output stream. " +
- "It should thus be the last statement in the XQuery.",
- new SequenceType[]{
- new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"),
- new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"),
- new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters")
- },
- new SequenceType(Type.EMPTY_SEQUENCE, Cardinality.EMPTY_SEQUENCE)),
- new FunctionSignature(
- new QName("stream-transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX),
- "Applies an XSL stylesheet to the node tree passed as first argument. The parameters are the same " +
- "as for the transform function. stream-transform can only be used within a servlet context. Instead " +
- "of returning the transformed document fragment, it directly streams its output to the servlet's output stream. " +
- "It should thus be the last statement in the XQuery.",
- new SequenceType[]{
- new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"),
- new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"),
- new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters"),
- new FunctionParameterSequenceType("attributes", Type.NODE, Cardinality.ZERO_OR_ONE, "Attributes to pass to the transformation factory"),
- new FunctionParameterSequenceType("serialization-options", Type.STRING, Cardinality.ZERO_OR_ONE, "The serialization options")},
- new SequenceType(Type.EMPTY_SEQUENCE, Cardinality.EMPTY_SEQUENCE))
- };
-
- private static final Logger logger = LogManager.getLogger(Transform.class);
-
- private boolean stopOnError = true;
- private boolean stopOnWarn = false;
-
- public Transform(XQueryContext context, FunctionSignature signature) {
+ private static final FunctionParameterSequenceType FS_PARAM_INPUT = optManyParam("input", Type.NODE, "A sequence of nodes to transform");
+ private static final FunctionParameterSequenceType FS_PARAM_STYLESHEET = param("stylesheet", Type.ITEM, "The XSLT Stylesheet. Should be either a document-node(), element(), or a URI (xs:anyURI or xs:string).");
+ private static final FunctionParameterSequenceType FS_PARAM_PARAMETERS = optParam("parameters", Type.ELEMENT, "Parameters to supply to the XSLT Stylesheet. The format of the value should be like: . There are two special parameters named \"exist:stop-on-warn\" and \"exist:stop-on-error\". If set to value \"yes\", an XQuery error will be raised if the XSLT processor reports a warning or error.");
+ private static final FunctionParameterSequenceType FS_PARAM_ATTRIBUTES = optParam("attributes", Type.ELEMENT, "Attributes to set on the transformer. The format of the value should be like: .");
+ private static final FunctionParameterSequenceType FS_PARAM_SERIALIZATION_OPTIONS = optParam("serialization-options", Type.STRING, "Options to set on the serializer. The format of the value should be like: 'method=xml omit-xml-declaration=yes'.");
+
+ private static final String FS_TRANSFORM_NAME = "transform";
+ static final FunctionSignature[] FS_TRANSFORM = functionSignatures(
+ FS_TRANSFORM_NAME,
+ "Applies the XSLT Stylesheet in $" + FS_PARAM_STYLESHEET.getAttributeName() + " to each node in $" + FS_PARAM_INPUT.getAttributeName() + ".",
+ returnsOptMany(Type.NODE, "The transformed nodes"),
+ arities(
+ arity(
+ FS_PARAM_INPUT,
+ FS_PARAM_STYLESHEET,
+ FS_PARAM_PARAMETERS
+ ),
+ arity(
+ FS_PARAM_INPUT,
+ FS_PARAM_STYLESHEET,
+ FS_PARAM_PARAMETERS,
+ FS_PARAM_ATTRIBUTES,
+ FS_PARAM_SERIALIZATION_OPTIONS
+ )
+ )
+ );
+
+ private static final String FS_STREAM_TRANSFORM_NAME = "stream-transform";
+ static final FunctionSignature[] FS_STREAM_TRANSFORM = functionSignatures(
+ FS_STREAM_TRANSFORM_NAME,
+ "Similarly to " + FS_TRANSFORM[0].getName().getExtendedStringValue() + ", this applies the XSLT Stylesheet in $" + FS_PARAM_STYLESHEET.getAttributeName() + " to each node in $" + FS_PARAM_INPUT.getAttributeName() + ". However the output is streamed directly to the current HTTP response. Note this function can only be used in a HTTP context.",
+ returnsNothing(),
+ arities(
+ arity(
+ FS_PARAM_INPUT,
+ FS_PARAM_STYLESHEET,
+ FS_PARAM_PARAMETERS
+ ),
+ arity(
+ FS_PARAM_INPUT,
+ FS_PARAM_STYLESHEET,
+ FS_PARAM_PARAMETERS,
+ FS_PARAM_ATTRIBUTES,
+ FS_PARAM_SERIALIZATION_OPTIONS
+ )
+ )
+ );
+
+ private static final StringValue OUTPUT_KEY = new StringValue("output");
+ private final org.exist.xquery.functions.fn.transform.Transform transform;
+
+ public Transform(final XQueryContext context, final FunctionSignature signature) {
super(context, signature);
+ this.transform = new org.exist.xquery.functions.fn.transform.Transform(this);
}
- /* (non-Javadoc)
- * @see org.exist.xquery.BasicFunction#eval(org.exist.xquery.value.Sequence[], org.exist.xquery.value.Sequence)
- */
- public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException {
-
- final Properties attributes = new Properties();
- final Properties serializationProps = new Properties();
- final Properties stylesheetParams = new Properties();
-
- // Parameter 1 & 2
- final Sequence inputNode = args[0];
- Item stylesheetItem = args[1].itemAt(0);
- if (stylesheetItem instanceof StringValue sv && sv.toString().startsWith("/db/")) {
- // adjust /db like URI to xmldb:exist:///db/ like URI
- stylesheetItem = new StringValue(XmldbURI.EMBEDDED_SERVER_URI_PREFIX + sv.toString().substring(3));
- }
-
- // Parse 3rd parameter
- final Node options = args[2].isEmpty() ? null : ((NodeValue) args[2].itemAt(0)).getNode();
- if (options != null) {
- stylesheetParams.putAll(parseParameters(options));
- }
-
- // Parameter 4 when present
- if (getArgumentCount() >= 4) {
- final Sequence attrs = args[3];
- attributes.putAll(extractAttributes(attrs));
- }
-
- // Parameter 5 when present
- if (getArgumentCount() >= 5) {
- //extract serialization options
- final Sequence serOpts = args[4];
- serializationProps.putAll(extractSerializationProperties(serOpts));
+ @Override
+ public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
+ final Sequence inputSequence = args[0];
+ final Either stylesheet = processStylesheetArgument(args[1].itemAt(0));
+ // Parse any parameters
+ final Parameters parameters;
+ if (args[2].isEmpty()) {
+ parameters = new Parameters(Map.empty(), false, false);
} else {
- context.checkOptions(serializationProps);
+ parameters = processParametersArgument((Element) ((NodeValue) args[2].itemAt(0)).getNode());
}
- boolean expandXIncludes =
- "yes".equals(serializationProps.getProperty(EXistOutputKeys.EXPAND_XINCLUDES, "yes"));
-
-
- final XSLTErrorsListener errorListener =
- new XSLTErrorsListener(stopOnError, stopOnWarn) {
- @Override
- protected void raiseError(final String error, final TransformerException ex) throws XPathException {
- throw new XPathException(Transform.this, error, ex);
- }
- };
-
- // Setup handler and error listener
- final TransformerHandler handler = createHandler(stylesheetItem, stylesheetParams, attributes, errorListener);
-
-
- if (isCalledAs("transform")) {
- //transform:transform()
-
- final ValueSequence seq = new ValueSequence();
- context.pushDocumentContext();
+ // Parse any TransformerFactory attributes
+ @Nullable List> attributes = null;
+ if (getArgumentCount() > 3 && !args[3].isEmpty()) {
+ attributes = processAttributesArgument((Element) ((NodeValue) args[3].itemAt(0)).getNode());
+ }
- try {
- final MemTreeBuilder builder = context.getDocumentBuilder();
- final DocumentBuilderReceiver builderReceiver = new DocumentBuilderReceiver(this, builder, true);
- final SAXResult result = new SAXResult(builderReceiver);
- result.setLexicalHandler(builderReceiver); //preserve comments etc... from xslt output
- handler.setResult(result);
- final Receiver receiver = new ReceiverToSAX(handler);
- final Serializer serializer = context.getBroker().borrowSerializer();
- try {
- serializer.setProperties(serializationProps);
- serializer.setReceiver(receiver, true);
- if (expandXIncludes) {
- String xiPath = serializationProps.getProperty(EXistOutputKeys.XINCLUDE_PATH);
- if (xiPath != null && !xiPath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
- final Path f = Paths.get(xiPath).normalize();
- if (!f.isAbsolute()) {
- xiPath = Paths.get(context.getModuleLoadPath(), xiPath).normalize().toAbsolutePath().toString();
- }
- } else {
- xiPath = context.getModuleLoadPath();
- }
- serializer.getXIncludeFilter().setModuleLoadPath(xiPath);
+ // Parse any serialization options
+ final Properties serializationOptions;
+ final boolean expandXincludes;
+ @Nullable final String xincludePath;
+ if (getArgumentCount() > 4 && !args[4].isEmpty()) {
+ serializationOptions = processSerializationOptionsArgument(args[4].itemAt(0).getStringValue());
+ expandXincludes = "yes".equals(serializationOptions.getProperty(EXistOutputKeys.EXPAND_XINCLUDES, "yes"));
+ if (expandXincludes) {
+ @Nullable String xiPath = serializationOptions.getProperty(EXistOutputKeys.XINCLUDE_PATH);
+ if (xiPath != null && !xiPath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
+ final Path f = Paths.get(xiPath).normalize();
+ if (!f.isAbsolute()) {
+ xiPath = Paths.get(context.getModuleLoadPath(), xiPath).normalize().toAbsolutePath().toString();
}
- serializer.toSAX(inputNode, 1, inputNode.getItemCount(), false, false, 0, 0);
-
- } catch (final Exception e) {
- throw new XPathException(this, "Exception while transforming node: " + e.getMessage(), e);
- } finally {
- context.getBroker().returnSerializer(serializer);
}
-
- errorListener.checkForErrors();
- Node next = builder.getDocument().getFirstChild();
- while (next != null) {
- seq.add((NodeValue) next);
- next = next.getNextSibling();
- }
-
- return seq;
- } finally {
- context.popDocumentContext();
+ xincludePath = xiPath;
+ } else {
+ xincludePath = null;
}
-
} else {
- //transform:stream-transform()
+ serializationOptions = new Properties();
+ expandXincludes = true;
+ xincludePath = null;
+ }
- final Optional maybeResponse = Optional.ofNullable(context.getHttpContext())
- .map(XQueryContext.HttpContext::getResponse);
+ // Get a Saxon configuration
+ final SaxonConfiguration saxonConfiguration = SaxonConfiguration.getConfiguration(getContext().getBroker().getBrokerPool().getConfiguration(), attributes);
- if (!maybeResponse.isPresent()) {
- throw new XPathException(this, ErrorCodes.XPDY0002, "No response object found in the current XQuery context.");
- }
+ // Perform the transformation
+ final XSLTErrorsListener errorListener = new StopErrorListener(this, parameters.stopOnError, parameters.stopOnWarn);
+ if (isCalledAs(FS_TRANSFORM_NAME)) {
+ return transform(saxonConfiguration, inputSequence, stylesheet, parameters, contextSequence, expandXincludes, xincludePath, errorListener);
- final ResponseWrapper response = maybeResponse.get();
- if (!"org.exist.http.servlets.HttpResponseWrapper".equals(response.getClass().getName())) {
- throw new XPathException(this, ErrorCodes.XPDY0002, signatures[1] +
- " can only be used within the EXistServlet or XQueryServlet");
+ } else if (isCalledAs(FS_STREAM_TRANSFORM_NAME)) {
+ if (context.getHttpContext() == null) {
+ throw new XPathException(this, ErrorCodes.W3CErrorCode.XPDY0002.getErrorCode(), "The function " + FS_STREAM_TRANSFORM[0].getName() + " requires an HTTP context");
}
-
- //setup the response correctly
- final String mediaType = handler.getTransformer().getOutputProperty("media-type");
- final String encoding = handler.getTransformer().getOutputProperty("encoding");
+ final ResponseWrapper response = context.getHttpContext().getResponse();
+ @Nullable final String mediaType = serializationOptions.getProperty(OutputKeys.MEDIA_TYPE);
if (mediaType != null) {
- if (encoding == null) {
- response.setContentType(mediaType);
- } else {
+ @Nullable final String encoding = serializationOptions.getProperty(OutputKeys.ENCODING);
+ if (encoding != null) {
response.setContentType(mediaType + "; charset=" + encoding);
+ } else {
+ response.setContentType(mediaType);
}
}
- //do the transformation
- try {
- final OutputStream os = new BufferedOutputStream(response.getOutputStream());
- final StreamResult result = new StreamResult(os);
- handler.setResult(result);
- final Serializer serializer = context.getBroker().borrowSerializer();
- Receiver receiver = new ReceiverToSAX(handler);
-
- try {
- serializer.setProperties(serializationProps);
- if (expandXIncludes) {
- XIncludeFilter xinclude = new XIncludeFilter(serializer, receiver);
- String xiPath = serializationProps.getProperty(EXistOutputKeys.XINCLUDE_PATH);
- if (xiPath != null) {
- final Path f = Paths.get(xiPath).normalize();
- if (!f.isAbsolute()) {
- xiPath = Paths.get(context.getModuleLoadPath(), xiPath).normalize().toAbsolutePath().toString();
- }
-
- } else {
- xiPath = context.getModuleLoadPath();
- }
-
- xinclude.setModuleLoadPath(xiPath);
- receiver = xinclude;
- }
- serializer.setReceiver(receiver);
- serializer.toSAX(inputNode);
-
- } catch (final Exception e) {
- throw new XPathException(this, "Exception while transforming node: " + e.getMessage(), e);
- } finally {
- context.getBroker().returnSerializer(serializer);
- }
-
- errorListener.checkForErrors();
- os.close();
+ try (final Writer writer = new OutputStreamWriter(response.getOutputStream())) {
+ final Sequence results = transform(saxonConfiguration, inputSequence, stylesheet, parameters, contextSequence, expandXincludes, xincludePath, errorListener);
+ final XQuerySerializer xquerySerializer = new XQuerySerializer(getContext().getBroker(), serializationOptions, writer);
+ xquerySerializer.serialize(results);
- //commit the response
+ // ensure the response is fully written
response.flushBuffer();
- } catch (final IOException e) {
- throw new XPathException(this, "IO exception while transforming node: " + e.getMessage(), e);
+ } catch (final IOException | SAXException e) {
+ throw new XPathException(this, "IO error while calling "+ FS_STREAM_TRANSFORM[0].getName() + ": " + e.getMessage(), e);
}
+
return Sequence.EMPTY_SEQUENCE;
+
+ } else {
+ throw new XPathException(this, "Unknown function signature: " + getSignature());
}
}
- /**
- * @param stylesheetItem
- * @param options
- * @param attributes Attributes to set on the Transformer Factory
- * @throws TransformerFactoryConfigurationError
- * @throws XPathException
- */
- private TransformerHandler createHandler(
- Item stylesheetItem,
- Properties options,
- Properties attributes,
- XSLTErrorsListener errorListener
- )
- throws TransformerFactoryConfigurationError, XPathException
- {
-
- boolean useCache = true;
- final Object property = context.getBroker().getConfiguration().getProperty(TransformerFactoryAllocator.PROPERTY_CACHING_ATTRIBUTE);
- if (property != null) {
- useCache = (Boolean) property;
+ private Sequence transform(final SaxonConfiguration saxonConfiguration, final Sequence inputSequence, final Either stylesheet, final Parameters parameters, final Sequence contextSequence, final boolean expandXincludes, @Nullable final String xincludePath, final XSLTErrorsListener errorListener) throws XPathException {
+ final Sequence results = new ValueSequence();
+
+ // setup the stylesheet, parameters, and vendor options for the transformation
+ final IMap transformOptions = MapType.newLinearMap(getContext().getDefaultCollator());
+ stylesheet.fold(location -> Options.STYLESHEET_LOCATION.set(transformOptions, location), node -> Options.STYLESHEET_NODE.set(transformOptions, node));
+ Options.STYLESHEET_PARAMS.set(transformOptions, new MapType(this, getContext(), parameters.stylesheetParameters, Type.QNAME));
+ final IMap vendorOptions = MapType.newLinearMap(getContext().getDefaultCollator());
+ Options.EXPAND_XINCLUDES.set(vendorOptions, expandXincludes ? BooleanValue.TRUE : BooleanValue.FALSE);
+ if (xincludePath != null) {
+ Options.XINCLUDE_PATH.set(vendorOptions, new StringValue(xincludePath));
}
+ Options.VENDOR_OPTIONS.set(transformOptions, new MapType(this, getContext(), vendorOptions.forked(), Type.QNAME));
- TransformerHandler handler;
- try {
- Stylesheet stylesheet = null;
- if (Type.subTypeOf(stylesheetItem.getType(), Type.NODE)) {
- final NodeValue stylesheetNode = (NodeValue) stylesheetItem;
- // if the passed node is a document node or document root element,
- // we construct an XMLDB URI and use the caching implementation.
- if (stylesheetNode.getImplementationType() == NodeValue.PERSISTENT_NODE) {
- final NodeProxy root = (NodeProxy) stylesheetNode;
- if (root.getNodeId() == NodeId.DOCUMENT_NODE || root.getNodeId().getTreeLevel() == 1) {
+ final SequenceIterator inputSequenceIterator = inputSequence.iterate();
+ while (inputSequenceIterator.hasNext()) {
+ NodeValue inputItem = (NodeValue) inputSequenceIterator.nextItem();
- final String uri = XmldbURI.XMLDB_URI_PREFIX + context.getBroker().getBrokerPool().getId() + "://" + root.getOwnerDocument().getURI();
-
- stylesheet = TemplatesFactory.stylesheet(uri, context.getModuleLoadPath(), attributes, useCache);
- }
- }
- if (stylesheet == null) {
- stylesheet = TemplatesFactory.stylesheet(
- getContext().getBroker(),
- stylesheetNode,
- context.getModuleLoadPath()
- );
- }
- } else {
- String baseUri = context.getModuleLoadPath();
- if (stylesheetItem instanceof Document) {
- baseUri = ((Document) stylesheetItem).getDocumentURI();
-
- /*
- * This must be checked because in the event the stylesheet is
- * an in-memory document, it will cause an NPE
- */
- if (baseUri == null) {
- baseUri = context.getModuleLoadPath();
+ // setup the source node for the transformation
+ if (((INodeHandle>)inputItem).getNodeType() != Node.DOCUMENT_NODE) {
+ // NOTE(AR) if the node is not a document we have to wrap it in a document to preserve the previous (bad) behaviour of the transform:transform function
+ try {
+ final DocumentBuilderReceiver builder = new DocumentBuilderReceiver(this);
+ builder.startDocument();
+ if (inputItem instanceof NodeProxy) {
+ builder.addReferenceNode((NodeProxy) inputItem);
} else {
- baseUri = baseUri.substring(0, baseUri.lastIndexOf('/'));
+ inputItem.copyTo(getContext().getBroker(), builder);
}
+ builder.endDocument();
+ inputItem = (NodeValue) builder.getDocument();
+ } catch (final SAXException e) {
+ throw new XPathException(this, "Unable to wrap node type: " + inputItem.getNode().getNodeType() + " in an in-memory document");
}
+ }
+ Options.SOURCE_NODE.set(transformOptions, inputItem);
- final String uri = stylesheetItem.getStringValue();
+ final Convert.ToSaxon toSaxon = new Convert.ToSaxon(saxonConfiguration.getProcessor());
+ final Options options = new Options(this, saxonConfiguration, toSaxon, new MapType(this, getContext(), transformOptions.forked(), Type.STRING));
- stylesheet = TemplatesFactory.stylesheet(uri, baseUri, attributes, useCache);
- }
+ context.pushDocumentContext();
+ try {
+ final MapType transformResult = transform.eval(saxonConfiguration, toSaxon, options, contextSequence, errorListener);
- handler = stylesheet.newTransformerHandler(getContext().getBroker(), errorListener);
+ errorListener.checkForErrors();
- if (options != null) {
- setParameters(options, handler.getTransformer());
- }
+ final Sequence principalResultDocument = transformResult.get(OUTPUT_KEY);
+ if (!principalResultDocument.isEmpty()) {
+ final Item principalResultItem = principalResultDocument.itemAt(0);
+ if (principalResultItem instanceof INodeHandle> && ((INodeHandle>) principalResultItem).getNodeType() == Node.DOCUMENT_NODE) {
+ // NOTE(AR) if the result is a document we have to unwrap its children to preserve the previous (bad) behaviour of the transform:transform function
+ @Nullable Node node = ((Document) principalResultItem).getFirstChild();
+ while (node != null) {
+ results.add((NodeValue) node);
+ node = node.getNextSibling();
+ }
- } catch (final Exception e) {
- if (e instanceof XPathException) {
- throw (XPathException) e;
+ } else {
+ results.add(principalResultItem);
+ }
+ }
+ } finally {
+ context.popDocumentContext();
}
- throw new XPathException(this, "Unable to set up transformer: " + e.getMessage(), e);
}
- return handler;
+
+ return results;
}
- private Properties extractSerializationProperties(final Sequence serOpts) throws XPathException {
+ private Properties processSerializationOptionsArgument(final String serializationOptions) throws XPathException {
final Properties serializationProps = new Properties();
- if (!serOpts.isEmpty()) {
- final String[] contents = Option.tokenize(serOpts.getStringValue());
- for (String content : contents) {
- final String[] pair = Option.parseKeyValuePair(content);
- if (pair == null) {
- throw new XPathException(this, "Found invalid serialization option: " + content);
- }
- logger.info("Setting serialization property: {} = {}", pair[0], pair[1]);
- serializationProps.setProperty(pair[0], pair[1]);
+ final String[] options = Option.tokenize(serializationOptions);
+ for (final String option : options) {
+ final String[] nameValue = Option.parseKeyValuePair(option);
+ if (nameValue == null) {
+ throw new XPathException(this, "Found invalid serialization option: " + option);
}
+ serializationProps.setProperty(nameValue[0], nameValue[1]);
}
return serializationProps;
}
- private Properties extractAttributes(final Sequence attrs) throws XPathException {
- if (attrs.isEmpty()) {
- return new Properties();
- } else {
- return parseElementParam(((NodeValue) attrs.itemAt(0)).getNode(), "attributes", "attr");
+ private @Nullable List> processAttributesArgument(final Element attributes) throws XPathException {
+ @Nullable List> transformerFactoryAttributes = null;
+
+ final NodeList attrs = attributes.getElementsByTagName("attr");
+ for (int i = 0; i < attrs.getLength(); i++) {
+ final Element attr = (Element) attrs.item(i);
+ final String name = attr.getAttribute("name");
+ final String value = attr.getAttribute("value");
+ if (name.isEmpty()) {
+ throw new XPathException(this, "Attributes name attribute is missing");
+ } else {
+ if (transformerFactoryAttributes == null) {
+ transformerFactoryAttributes = new ArrayList<>();
+ }
+ transformerFactoryAttributes.add(Tuple(name, value));
+ }
}
+
+ return transformerFactoryAttributes;
}
- private Properties parseParameters(final Node options) throws XPathException {
- return parseElementParam(options, "parameters", "param");
+ private Parameters processParametersArgument(final Element parameters) throws XPathException {
+ final IMap stylesheetParameters = MapType.newLinearMap(getContext().getDefaultCollator());
+ boolean stopOnWarn = false;
+ boolean stopOnError = false;
+
+ final NodeList params = parameters.getElementsByTagName("param");
+ for (int i = 0; i < params.getLength(); i++) {
+ final Element param = (Element) params.item(i);
+ final String name = param.getAttribute("name");
+ final String value = param.getAttribute("value");
+ if (name.isEmpty()) {
+ throw new XPathException(this, "Parameters name attribute is missing");
+ } else if ("exist:stop-on-warn".equals(name)) {
+ stopOnWarn = "yes".equals(value);
+ } else if ("exist:stop-on-error".equals(name)) {
+ stopOnError = "yes".equals(value);
+ } else {
+ stylesheetParameters.put(new QNameValue(this, getContext(), name), new StringValue(this, value));
+ }
+ }
+
+ return new Parameters(stylesheetParameters.forked(), stopOnWarn, stopOnError);
}
- private Properties parseElementParam(final Node elementParam, final String container, final String param) throws XPathException {
- final Properties props = new Properties();
- if (elementParam.getNodeType() == Node.ELEMENT_NODE && elementParam.getLocalName().equals(container)) {
- Node child = elementParam.getFirstChild();
- while (child != null) {
- if (child.getNodeType() == Node.ELEMENT_NODE && child.getLocalName().equals(param)) {
- final Element elem = (Element) child;
- final String name = elem.getAttribute("name");
- final String value = elem.getAttribute("value");
- if (name.isEmpty() || value.isEmpty()) {
- throw new XPathException(this, "Name or value attribute missing");
- }
+ private Either processStylesheetArgument(final Item stylesheetArgument) throws XPathException {
+ if (stylesheetArgument instanceof NodeValue) {
+ return Either.Right((NodeValue) stylesheetArgument);
- if ("exist:stop-on-warn".equals(name)) {
- stopOnWarn = "yes".equals(value);
- } else if ("exist:stop-on-error".equals(name)) {
- stopOnError = "yes".equals(value);
- } else {
- props.setProperty(name, value);
- }
- }
- child = child.getNextSibling();
- }
+ } else if (stylesheetArgument instanceof AnyURIValue) {
+ return Either.Left((StringValue) stylesheetArgument.convertTo(Type.STRING));
+
+ } else if (stylesheetArgument instanceof StringValue) {
+ return Either.Left((StringValue) stylesheetArgument);
}
- return props;
+
+ throw new XPathException(this, "The parameter $" + FS_PARAM_STYLESHEET.getAttributeName() + " must be of type document-node(), element(), xs:anyURI, or xs:string");
}
- private void setParameters(Properties parameters, Transformer handler) {
- for (Object o : parameters.keySet()) {
- final String key = (String) o;
- handler.setParameter(key, parameters.getProperty(key));
+ private static class Parameters {
+ final IMap stylesheetParameters;
+ final boolean stopOnWarn;
+ final boolean stopOnError;
+
+ private Parameters(final IMap stylesheetParameters, final boolean stopOnWarn, final boolean stopOnError) {
+ this.stylesheetParameters = stylesheetParameters;
+ this.stopOnWarn = stopOnWarn;
+ this.stopOnError = stopOnError;
+ }
+ }
+
+ private static class StopErrorListener extends XSLTErrorsListener {
+ private final Expression callingExpression;
+
+ private StopErrorListener(final Expression callingExpression, final boolean stopOnError, final boolean stopOnWarn) {
+ super(stopOnError, stopOnWarn);
+ this.callingExpression = callingExpression;
+ }
+
+ @Override
+ protected void raiseError(final String error, final TransformerException ex) throws XPathException {
+ throw new XPathException(callingExpression, error, ex);
}
}
}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java b/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java
index a2f3564de8..8073e54d0d 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -23,57 +47,68 @@
import java.util.List;
import java.util.Map;
+
+import org.exist.dom.QName;
import org.exist.xquery.AbstractInternalModule;
+import org.exist.xquery.FunctionDSL;
import org.exist.xquery.FunctionDef;
+import org.exist.xquery.FunctionSignature;
+import org.exist.xquery.value.FunctionParameterSequenceType;
+import org.exist.xquery.value.FunctionReturnSequenceType;
+
+import static org.exist.xquery.FunctionDSL.functionDefs;
/**
* Module function definitions for transform module.
*
+ * @author Adam Retter
* @author Wolfgang Meier
* @author ljo
*/
public class TransformModule extends AbstractInternalModule {
- public final static String NAMESPACE_URI = "http://exist-db.org/xquery/transform";
+ public static final String NAMESPACE_URI = "http://exist-db.org/xquery/transform";
- public final static String PREFIX = "transform";
- public final static String INCLUSION_DATE = "2004-09-12";
- public final static String RELEASED_IN_VERSION = "pre eXist-1.0";
+ public static final String PREFIX = "transform";
+ public static final String INCLUSION_DATE = "2004-09-12";
+ public static final String RELEASED_IN_VERSION = "pre eXist-1.0";
- private final static FunctionDef[] functions = {
- new FunctionDef(Transform.signatures[0], Transform.class),
- new FunctionDef(Transform.signatures[1], Transform.class),
- new FunctionDef(Transform.signatures[2], Transform.class),
- new FunctionDef(Transform.signatures[3], Transform.class)
- };
+ private static final FunctionDef[] functions = functionDefs(
+ functionDefs(
+ Transform.class,
+ Transform.FS_TRANSFORM
+ ),
+ functionDefs(
+ Transform.class,
+ Transform.FS_STREAM_TRANSFORM
+ )
+ );
- public TransformModule(Map> parameters) {
+ public TransformModule(final Map> parameters) {
super(functions, parameters);
}
- /* (non-Javadoc)
- * @see org.exist.xquery.Module#getDescription()
- */
+ @Override
public String getDescription() {
return "A module for dealing with XSL transformations.";
}
- /* (non-Javadoc)
- * @see org.exist.xquery.Module#getNamespaceURI()
- */
+ @Override
public String getNamespaceURI() {
return NAMESPACE_URI;
}
- /* (non-Javadoc)
- * @see org.exist.xquery.Module#getDefaultPrefix()
- */
+ @Override
public String getDefaultPrefix() {
return PREFIX;
}
+ @Override
public String getReleaseVersion() {
return RELEASED_IN_VERSION;
}
+ static FunctionSignature[] functionSignatures(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType[][] variableParamTypes) {
+ return FunctionDSL.functionSignatures(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, variableParamTypes);
+ }
}
diff --git a/exist-core/src/main/java/org/exist/xslt/EXistDbInputSource.java b/exist-core/src/main/java/org/exist/xslt/EXistDbInputSource.java
index d3a2502b67..3c8706fa1a 100644
--- a/exist-core/src/main/java/org/exist/xslt/EXistDbInputSource.java
+++ b/exist-core/src/main/java/org/exist/xslt/EXistDbInputSource.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -38,13 +62,12 @@ public class EXistDbInputSource extends InputSource {
private final DBBroker broker;
private final DocumentImpl doc;
- public EXistDbInputSource(DBBroker broker, DocumentImpl doc) {
- super();
-
+ public EXistDbInputSource(final DBBroker broker, final DocumentImpl doc) {
+ super(doc.getBaseURI());
this.broker = broker;
this.doc = doc;
}
-
+
public DBBroker getBroker() {
return this.broker;
}
diff --git a/exist-core/src/main/java/org/exist/xslt/SaxonConfiguration.java b/exist-core/src/main/java/org/exist/xslt/SaxonConfiguration.java
new file mode 100644
index 0000000000..be08242d87
--- /dev/null
+++ b/exist-core/src/main/java/org/exist/xslt/SaxonConfiguration.java
@@ -0,0 +1,291 @@
+/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.exist.xslt;
+
+import com.evolvedbinary.j8fu.tuple.Tuple2;
+import org.exist.util.Configuration;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import net.jcip.annotations.ThreadSafe;
+import net.sf.saxon.s9api.Processor;
+import net.sf.saxon.trans.XPathException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.annotation.Nullable;
+import javax.xml.transform.stream.StreamSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author Adam Retter
+ * @author Alan Paxton
+ */
+@ThreadSafe
+public final class SaxonConfiguration {
+
+ private static final Logger LOG = LogManager.getLogger(SaxonConfiguration.class);
+
+ public static final String SAXON_CONFIGURATION_ELEMENT_NAME = "saxon";
+ public static final String SAXON_CONFIGURATION_FILE_ATTRIBUTE = "configuration-file";
+ public static final String SAXON_CONFIGURATION_FILE_PROPERTY = "saxon.configuration";
+ private static final String SAXON_DEFAULT_SAXON_CONFIG_FILE = "saxon-config.xml";
+
+ private static volatile SaxonConfiguration DEFAULT_SAXON_CONFIGURATION;
+
+ private static final Cache SAXON_CONFIGURATION_CACHE = Caffeine.newBuilder()
+ .maximumSize(8)
+ .weakValues()
+ .build();
+
+ private final @Nullable Path saxonConfigFilePath;
+ private final @Nullable List> configurationProperties;
+ private final net.sf.saxon.Configuration configuration;
+ private final Processor processor;
+
+ private SaxonConfiguration(@Nullable final Path saxonConfigFile, @Nullable final List> configurationProperties, final net.sf.saxon.Configuration configuration) {
+ this.saxonConfigFilePath = saxonConfigFile;
+ this.configurationProperties = configurationProperties != null ? Collections.unmodifiableList(configurationProperties) : null;
+ this.configuration = configuration;
+ this.processor = new Processor(configuration);
+ //TODO (AP) This is a better place to configure URI/Resource resolution for Saxon within Elemental, at present the configuration for Saxon to resolve xmldb:exist: URIs is restricted to fn:transform
+ }
+
+ /**
+ * Get the path to the Saxon config file, if a config file was used.
+ *
+ * @return the path to the Saxon config file, or null if not config file was used.
+ */
+ public @Nullable Path getSaxonConfigFilePath() {
+ return saxonConfigFilePath;
+ }
+
+ /**
+ * Get any additional Saxon configuration properties
+ *
+ * @return the additional Saxon configuration properties, or null if there are none.
+ */
+ public @Nullable List> getConfigurationProperties() {
+ return configurationProperties;
+ }
+
+ /**
+ * Get the Saxon API's {@link net.sf.saxon.Configuration} object.
+ *
+ * @return Saxon internal configuration object
+ */
+ public net.sf.saxon.Configuration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * Get the Saxon API's {@link Processor} through which Saxon operations
+ * such as transformation can be effected.
+ *
+ * @return the Saxon {@link Processor} associated with the configuration.
+ */
+ public Processor getProcessor() {
+ return processor;
+ }
+
+ /**
+ * Gets a Saxon configuration that is unique for the configuration and configuration properties.
+ *
+ * May either load a new configuration or return a cached Saxon configuration that can be re-used.
+ *
+ * @param elementalConfiguration the database configuration.
+ * @param configurationProperties any additional configuration properties for Saxon, or null.
+ *
+ * @return the Saxon configuration.
+ */
+ public static SaxonConfiguration getConfiguration(final Configuration elementalConfiguration, @Nullable final List> configurationProperties) {
+ final @Nullable Path saxonConfigFile = getSaxonConfigFile(elementalConfiguration);
+
+ if (saxonConfigFile != null && configurationProperties == null) {
+ // NOTE(AR) the default Saxon configuration for Elemental has been requested
+ if (DEFAULT_SAXON_CONFIGURATION == null) {
+ synchronized (SaxonConfiguration.class) {
+ if (DEFAULT_SAXON_CONFIGURATION == null) {
+ DEFAULT_SAXON_CONFIGURATION = newSaxonConfiguration(saxonConfigFile, configurationProperties);
+ }
+ }
+ }
+ return DEFAULT_SAXON_CONFIGURATION;
+ }
+
+ final long cacheKey = cacheKey(saxonConfigFile, configurationProperties);
+ return SAXON_CONFIGURATION_CACHE.get(cacheKey, key -> newSaxonConfiguration(saxonConfigFile, configurationProperties));
+ }
+
+ /**
+ * Calculate a key for {@link #SAXON_CONFIGURATION_CACHE}.
+ *
+ * @param saxonConfigFile The path to a Saxon configuration file, or null.
+ * @param configurationProperties any additional configuration properties for Saxon, or null.
+ *
+ * @return the cache key.
+ */
+ private static long cacheKey(final @Nullable Path saxonConfigFile, @Nullable final List> configurationProperties) {
+ return Objects.hash(saxonConfigFile, configurationProperties);
+ }
+
+ /**
+ * Load the Saxon {@link net.sf.saxon.Configuration} from a configuration file when it is first needed;
+ * if we cannot find a configuration file (and license) to give to Saxon, it (Saxon) may still be able to find
+ * something by searching in more "well-known to Saxon" locations.
+ *
+ * @param saxonConfigFile The path to a Saxon configuration file, or null.
+ * @param configurationProperties any additional configuration properties for Saxon, or null.
+ *
+ * @return a freshly loaded Saxon configuration
+ */
+ private static SaxonConfiguration newSaxonConfiguration(final @Nullable Path saxonConfigFile, @Nullable final List> configurationProperties) {
+ @Nullable net.sf.saxon.Configuration saxonConfiguration = null;
+ if (saxonConfigFile != null) {
+ saxonConfiguration = readSaxonConfigurationFile(saxonConfigFile);
+ }
+
+ if (saxonConfiguration == null) {
+ LOG.warn("Elemental could not find any Saxon configuration:\n" +
+ "No Saxon configuration file in configuration item " + SAXON_CONFIGURATION_FILE_PROPERTY + "\n" +
+ "No default Elemental Saxon configuration file " + SAXON_DEFAULT_SAXON_CONFIG_FILE);
+
+ saxonConfiguration = net.sf.saxon.Configuration.newConfiguration();
+ }
+
+ if (configurationProperties != null) {
+ for (final Tuple2 configurationProperty : configurationProperties) {
+ saxonConfiguration.setConfigurationProperty(configurationProperty._1, configurationProperty._2);
+ }
+ }
+
+ reportLicensedFeatures(saxonConfiguration);
+
+ return new SaxonConfiguration(saxonConfigFile, configurationProperties, saxonConfiguration);
+ }
+
+ private static @Nullable net.sf.saxon.Configuration readSaxonConfigurationFile(final Path saxonConfigFile) {
+ try (final InputStream is = Files.newInputStream(saxonConfigFile)) {
+ return net.sf.saxon.Configuration.readConfiguration(new StreamSource(is));
+ } catch (final XPathException | IOException e) {
+ LOG.warn("Saxon could not read the configuration file: " + saxonConfigFile + ", with error: " + e.getMessage(), e);
+ } catch (RuntimeException runtimeException) {
+ if (runtimeException.getCause() instanceof ClassNotFoundException e) {
+ LOG.warn("Saxon could not honour the configuration file: " + saxonConfigFile + ", with class not found error: " + e.getMessage() + ". You may need to install the SaxonPE or SaxonEE JAR in Elemental.");
+ } else {
+ throw runtimeException;
+ }
+ }
+ return null;
+ }
+
+ private static void reportLicensedFeatures(final net.sf.saxon.Configuration configuration) {
+ configuration.displayLicenseMessage();
+
+ final StringBuilder sb = new StringBuilder();
+ if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.SCHEMA_VALIDATION)) {
+ sb.append(" SCHEMA_VALIDATION");
+ }
+ if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.ENTERPRISE_XSLT)) {
+ sb.append(" ENTERPRISE_XSLT");
+ }
+ if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.ENTERPRISE_XQUERY)) {
+ sb.append(" ENTERPRISE_XQUERY");
+ }
+ if (configuration.isLicensedFeature(net.sf.saxon.Configuration.LicenseFeature.PROFESSIONAL_EDITION)) {
+ sb.append(" PROFESSIONAL_EDITION");
+ }
+ if (sb.isEmpty()) {
+ LOG.info("Saxon - no licensed features reported.");
+ } else {
+ LOG.info("Saxon - licensed features are" + sb + ".");
+ }
+ }
+
+ /**
+ * Resolve a possibly relative configuration file;
+ * if it is relative, it is relative to the current elemental configuration (conf.xml)
+ *
+ * @param elementalConfiguration configuration to which this file may be relative
+ * @param filename the file we are trying to resolve
+ * @return the input file, if it is absolute. a file relative to conf.xml, if the input file is relative
+ */
+ private static Path resolveConfigurationFile(final Configuration elementalConfiguration, final String filename) {
+ final Path configurationFile = Paths.get(filename);
+ if (configurationFile.isAbsolute()) {
+ return configurationFile;
+ }
+
+ final Optional configPath = elementalConfiguration.getConfigFilePath();
+ return configPath.map(p -> p.getParent().resolve(configurationFile)).orElse(configurationFile);
+ }
+
+ public static @Nullable Path getSaxonConfigFile(final Configuration elementalConfiguration) {
+ if (elementalConfiguration.getProperty(SAXON_CONFIGURATION_FILE_PROPERTY) instanceof String saxonConfigurationFile) {
+ final Path configurationFile = resolveConfigurationFile(elementalConfiguration, saxonConfigurationFile);
+ if (Files.isReadable(configurationFile)) {
+ return configurationFile;
+ } else {
+ LOG.warn("Configuration item " + SAXON_CONFIGURATION_FILE_PROPERTY + " : " + configurationFile +
+ " does not refer to a readable file. Continuing search for Saxon configuration.");
+ }
+ }
+
+ final Path configurationFile = resolveConfigurationFile(elementalConfiguration, SAXON_DEFAULT_SAXON_CONFIG_FILE);
+ if (Files.isReadable(configurationFile)) {
+ return configurationFile;
+ }
+
+ return null;
+ }
+}
diff --git a/exist-core/src/test/java/org/exist/config/SaxonConfigTest.java b/exist-core/src/test/java/org/exist/config/SaxonConfigTest.java
deleted file mode 100644
index db6996ee5c..0000000000
--- a/exist-core/src/test/java/org/exist/config/SaxonConfigTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * eXist-db Open Source Native XML Database
- * Copyright (C) 2001 The eXist-db Authors
- *
- * info@exist-db.org
- * http://www.exist-db.org
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-package org.exist.config;
-
-import org.exist.test.ExistEmbeddedServer;
-import org.junit.ClassRule;
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class SaxonConfigTest {
-
- @ClassRule
- public static final ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true);
-
- @Test
- public void configFromBroker() {
- final var brokerPool = existEmbeddedServer.getBrokerPool();
-
- final var existConfiguration = brokerPool.getConfiguration();
- assertThat(existConfiguration.getProperty("saxon.configuration")).isEqualTo("saxon-config.xml");
-
- final var saxonConfiguration = brokerPool.getSaxonConfiguration();
-
- // There is no way to install EE at the test/build phase.
- // Sanity check is to confirm this does indeed return "HE" (Home Edition).
- final var saxonProcessor = brokerPool.getSaxonProcessor();
- assertThat(saxonProcessor.getSaxonEdition()).isEqualTo("HE");
-
- final var saxonConfiguration2 = brokerPool.getSaxonConfiguration();
- assertThat(saxonConfiguration2).isSameAs(saxonConfiguration);
-
- }
-}
diff --git a/exist-core/src/test/java/org/exist/xquery/TransformTest.java b/exist-core/src/test/java/org/exist/xquery/TransformTest.java
index 7bbc2ef169..86d5a61229 100644
--- a/exist-core/src/test/java/org/exist/xquery/TransformTest.java
+++ b/exist-core/src/test/java/org/exist/xquery/TransformTest.java
@@ -73,19 +73,42 @@ public class TransformTest {
private static final String TEST_COLLECTION_NAME = "transform-test";
private Collection testCollection;
-
+
+
/**
* Tests relative path resolution when parsing stylesheets in
* the transform:transform function.
*/
@Test
- public void transform() throws XMLDBException {
- String query =
+ public void documentTransform() throws XMLDBException {
+ final String query =
+ "import module namespace transform='http://exist-db.org/xquery/transform';\n" +
+ "let $xml := document { }\n" +
+ "let $xsl := 'xmldb:exist:///db/" + TEST_COLLECTION_NAME + "/xsl1/1.xsl'\n" +
+ "return transform:transform($xml, $xsl, ())";
+ final String result = execQuery(query);
+ assertEquals("" +
+ "
").build();
+ expectQuery(ELEMENT_SAME_DIR_IMPORT_VIA_XMLDB_NODE_QUERY, expected);
}
@Test
@@ -247,6 +302,50 @@ public void identityMixedMemoryAndPersistentDom() throws XPathException, Permiss
expectQuery(IDENTITY_MIXED_XSLT_QUERY_5, expected);
}
+ @Test
+ public void xslMessageIsLogged() throws EXistException, PermissionDeniedException, IOException, XPathException {
+
+ // set a mock logger so we can capture the log output for our test
+ final Logger mockLogger = createMock(Logger.class);
+ Transform.setLogger(mockLogger);
+
+ // expectations
+ final Capture formatPattern = newCapture();
+ final Capture startTagCapture = newCapture();
+ final Capture logMessageCapture = newCapture();
+ mockLogger.info(capture(formatPattern), capture(startTagCapture), capture(logMessageCapture));
+
+ // reset mock state before test
+ replay(mockLogger);
+
+ // execute test
+ final String query =
+ "fn:transform(map {\n" +
+ " \"stylesheet-text\": '\n" +
+ " \n" +
+ " Hello from XSLT\n" +
+ " \n" +
+ " ',\n" +
+ " \"source-node\": document { }\n" +
+ "})?output";
+
+ final BrokerPool pool = existEmbeddedServer.getBrokerPool();
+ try (final DBBroker broker = pool.get(Optional.of(pool.getSecurityManager().getSystemSubject()));
+ final XQueryUtil.QueryResult queryResult = XQueryUtil.query(broker, new StringSource(query), false, null, null, null, null, null)) {
+ assertNotNull(queryResult.result);
+ }
+
+ // verify our expectations
+ verify(mockLogger);
+
+ // check our assertions about the log message
+ final String startTag = startTagCapture.getValue();
+ final String message = logMessageCapture.getValue();
+
+ assertEquals("", startTag);
+ assertEquals("Hello from XSLT", message);
+ }
+
private static void expectQuery(final String query, final Source expected) throws EXistException, XPathException, PermissionDeniedException, IOException {
final BrokerPool pool = existEmbeddedServer.getBrokerPool();
try(final DBBroker broker = pool.get(Optional.of(pool.getSecurityManager().getSystemSubject()));
diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.java
index 0a6f6b8409..65b23c62e0 100644
--- a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.java
+++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -123,7 +147,7 @@ public void resolution() throws XPathException, URISyntaxException {
*/
@Test
public void resolverObject() throws TransformerException {
- var resolver = new URIResolution.CompileTimeURIResolver(new XQueryContext(), null) {
+ var resolver = new URIResolution.CompileTimeURIResolver(null) {
@Override protected SourceWithLocation resolveDocument(final String location) {
return new SourceWithLocation("RESOLVED::" + location);
}
diff --git a/exist-core/src/test/java/org/exist/xquery/functions/transform/TransformTest.java b/exist-core/src/test/java/org/exist/xquery/functions/transform/TransformTest.java
index d12526f387..71849c92a0 100644
--- a/exist-core/src/test/java/org/exist/xquery/functions/transform/TransformTest.java
+++ b/exist-core/src/test/java/org/exist/xquery/functions/transform/TransformTest.java
@@ -388,9 +388,13 @@ private static void transform_twoNodesCountDescendants() throws EXistException,
assertNotNull(sequence);
assertTrue(sequence.hasOne());
+ final Item resultItem = sequence.itemAt(0);
+ assertTrue(resultItem instanceof org.exist.dom.memtree.ElementImpl);
+ final Document resultDocument = ((Element) resultItem).getOwnerDocument();
+ assertNotNull(resultDocument);
final Source expected = Input.fromString("21").build();
- final Source actual = Input.fromDocument(sequence.itemAt(0).toJavaObject(Node.class).getOwnerDocument()).build();
+ final Source actual = Input.fromDocument(resultDocument).build();
final Diff diff = DiffBuilder.compare(expected)
.withTest(actual)
diff --git a/exist-core/src/test/java/org/exist/xslt/SaxonConfigurationTest.java b/exist-core/src/test/java/org/exist/xslt/SaxonConfigurationTest.java
new file mode 100644
index 0000000000..591b699093
--- /dev/null
+++ b/exist-core/src/test/java/org/exist/xslt/SaxonConfigurationTest.java
@@ -0,0 +1,83 @@
+/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.exist.xslt;
+
+import net.sf.saxon.s9api.Processor;
+import org.exist.storage.BrokerPool;
+import org.exist.test.ExistEmbeddedServer;
+import org.exist.util.Configuration;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Adam Retter
+ * @author Alan Paxton
+ */
+public class SaxonConfigurationTest {
+
+ @ClassRule
+ public static final ExistEmbeddedServer EXIST_EMBEDDED_SERVER = new ExistEmbeddedServer(true, true);
+
+ @Test
+ public void configFromBroker() {
+ final BrokerPool brokerPool = EXIST_EMBEDDED_SERVER.getBrokerPool();
+
+ final Configuration elementalConfiguration = brokerPool.getConfiguration();
+ assertThat(elementalConfiguration.getProperty("saxon.configuration")).isEqualTo("saxon-config.xml");
+
+ final SaxonConfiguration saxonConfiguration = SaxonConfiguration.getConfiguration(elementalConfiguration, null);
+
+ // There is no way to install EE at the test/build phase.
+ // Sanity check is to confirm this does indeed return "HE" (Home Edition).
+ final Processor saxonProcessor = saxonConfiguration.getProcessor();
+ assertThat(saxonProcessor.getSaxonEdition()).isEqualTo("HE");
+
+ final SaxonConfiguration saxonConfiguration2 = SaxonConfiguration.getConfiguration(elementalConfiguration, null);
+ assertThat(saxonConfiguration2).isSameAs(saxonConfiguration);
+ }
+}
diff --git a/exist-core/src/test/java/xquery/xquery3/XQuery3Tests.java b/exist-core/src/test/java/xquery/xquery3/XQuery3Tests.java
index cdf0ba44d4..cd328a0c9b 100644
--- a/exist-core/src/test/java/xquery/xquery3/XQuery3Tests.java
+++ b/exist-core/src/test/java/xquery/xquery3/XQuery3Tests.java
@@ -1,4 +1,28 @@
/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
+ * The original license header is included below.
+ *
+ * =====================================================================
+ *
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
@@ -27,7 +51,6 @@
@RunWith(XSuite.class)
@XSuite.XSuiteFiles({
"src/test/xquery/xquery3",
- "src/test/xquery/xquery3/transform",
// To add an individual test or only run a specific set of tests -
//"src/test/xquery/xquery3/serialize.xql",
})
diff --git a/exist-core/src/test/java/xquery/xquery3/transform/XQuery3TransformTests.java b/exist-core/src/test/java/xquery/xquery3/transform/XQuery3TransformTests.java
new file mode 100644
index 0000000000..89a789be9b
--- /dev/null
+++ b/exist-core/src/test/java/xquery/xquery3/transform/XQuery3TransformTests.java
@@ -0,0 +1,31 @@
+/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package xquery.xquery3.transform;
+
+import org.exist.test.runner.XSuite;
+import org.junit.runner.RunWith;
+
+@RunWith(XSuite.class)
+@XSuite.XSuiteFiles({
+ "src/test/xquery/xquery3/transform"
+})
+public class XQuery3TransformTests {
+}
diff --git a/exist-core/src/test/xquery/xinclude/xinclude.xml b/exist-core/src/test/xquery/xinclude/xinclude.xml
index 542751cfa1..f1a0153b5c 100644
--- a/exist-core/src/test/xquery/xinclude/xinclude.xml
+++ b/exist-core/src/test/xquery/xinclude/xinclude.xml
@@ -1,6 +1,30 @@