From eb0becd81ebda257ba57089e4e2257952469cbf6 Mon Sep 17 00:00:00 2001 From: Laurettta Date: Mon, 3 Aug 2026 10:10:36 +0100 Subject: [PATCH 1/3] [bugfix] Log xsl:message output from fn:transform() to Elemental's log Closes https://github.com/evolvedbinary/elemental/issues/234 --- .../functions/fn/transform/Transform.java | 73 ++++++++++++++++++- .../fn/transform/FunTransformITTest.java | 54 ++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) 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..6e17cfcee7 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,6 +53,9 @@ 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.QName; @@ -67,8 +70,10 @@ import org.w3c.dom.Node; 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 java.net.URI; @@ -79,6 +84,7 @@ 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,7 +110,7 @@ */ 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() { @@ -161,6 +167,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro } final Xslt30Transformer xslt30Transformer = xsltExecutable.load30(); + xslt30Transformer.setMessageListener(new XsltMessageListener(context.getBroker().getBrokerPool().getSaxonProcessor(), getLogger())); options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName()))); xslt30Transformer.setInitialTemplateParameters(options.templateParams, false); @@ -464,6 +471,19 @@ private static Optional getSourceNode(final Optional sourceNo 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 { private final Logger logger; @@ -532,4 +552,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/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java index 43b9a63732..ea285e5ce1 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java @@ -21,6 +21,8 @@ package org.exist.xquery.functions.fn.transform; import com.evolvedbinary.j8fu.tuple.Tuple2; +import org.apache.logging.log4j.Logger; +import org.easymock.Capture; import org.exist.EXistException; import org.exist.collections.Collection; import org.exist.security.PermissionDeniedException; @@ -49,9 +51,17 @@ import javax.xml.transform.Source; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple; +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.newCapture; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; import static org.junit.Assert.*; /** @@ -247,6 +257,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())); From 36aac2783bf98b95afa250b80a3d249c69a1589c Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 1 Sep 2026 15:02:35 +0200 Subject: [PATCH 2/3] [optimize] Detect Document node type if possible --- .../src/main/java/org/exist/dom/persistent/NodeProxy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From 8906d3ee2c43370ab5aa1178ec6aec9ff72ef33c Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Thu, 10 Sep 2026 17:29:56 +0100 Subject: [PATCH 3/3] [refactor] Switch transform:transform to use the same implementation as fn:transform --- exist-core/pom.xml | 30 +- .../src/main/java/org/exist/Namespaces.java | 1 + .../reference/AbstractReferenceNodeImpl.java | 1 - .../java/org/exist/storage/BrokerPool.java | 11 - .../storage/serializers/FeatureKeys.java | 41 ++ .../serializers/NodeValueInputSource.java | 85 +++ .../exist/storage/serializers/Serializer.java | 73 +- .../java/org/exist/util/Configuration.java | 1 + .../org/exist/util/SaxonConfiguration.java | 188 ----- .../main/java/org/exist/xquery/Context.java | 2 +- .../java/org/exist/xquery/XQueryContext.java | 2 +- .../xquery/functions/fn/FnTransform.java | 39 +- .../xquery/functions/fn/FunAnalyzeString.java | 3 +- .../exist/xquery/functions/fn/FunMatches.java | 29 +- .../exist/xquery/functions/fn/FunReplace.java | 3 +- .../functions/fn/transform/Convert.java | 14 +- .../functions/fn/transform/Options.java | 174 +++-- .../functions/fn/transform/Transform.java | 434 +++++++---- .../functions/fn/transform/URIResolution.java | 61 +- .../exist/xquery/functions/map/MapType.java | 2 +- .../xquery/functions/transform/Transform.java | 687 ++++++++---------- .../functions/transform/TransformModule.java | 75 +- .../org/exist/xslt/EXistDbInputSource.java | 31 +- .../org/exist/xslt/SaxonConfiguration.java | 291 ++++++++ .../org/exist/config/SaxonConfigTest.java | 54 -- .../java/org/exist/xquery/TransformTest.java | 96 ++- .../functions/fn/transform/ConvertTest.java | 7 +- .../fn/transform/FunTransformITTest.java | 77 +- .../fn/transform/FunTransformTest.java | 26 +- .../functions/transform/TransformTest.java | 6 +- .../exist/xslt/SaxonConfigurationTest.java | 83 +++ .../java/xquery/xquery3/XQuery3Tests.java | 25 +- .../transform/XQuery3TransformTests.java | 31 + .../src/test/xquery/xinclude/xinclude.xml | 44 ++ 34 files changed, 1774 insertions(+), 953 deletions(-) create mode 100644 exist-core/src/main/java/org/exist/storage/serializers/FeatureKeys.java create mode 100644 exist-core/src/main/java/org/exist/storage/serializers/NodeValueInputSource.java delete mode 100644 exist-core/src/main/java/org/exist/util/SaxonConfiguration.java create mode 100644 exist-core/src/main/java/org/exist/xslt/SaxonConfiguration.java delete mode 100644 exist-core/src/test/java/org/exist/config/SaxonConfigTest.java create mode 100644 exist-core/src/test/java/org/exist/xslt/SaxonConfigurationTest.java create mode 100644 exist-core/src/test/java/xquery/xquery3/transform/XQuery3TransformTests.java 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.xml src/test/xquery/pi.xqm src/test/xquery/securitymanager/acl.xqm + src/test/java/xquery/xquery3/transform/XQuery3TransformTests.java src/main/java/org/exist/dom/memtree/DocumentTypeImpl.java src/test/resources/org/exist/dom/memtree/simple.xhtml src/main/java/org/exist/dom/memtree/reference/AbstractReferenceCharacterData.java @@ -755,7 +756,9 @@ src/main/java/org/exist/storage/io/VariableByteFilterInputStream.java src/main/java/org/exist/storage/io/VariableByteFilterOutputStream.java src/main/java/org/exist/storage/io/VariableByteOutput.java + src/main/java/org/exist/storage/serializers/FeatureKeys.java src/test/java/org/exist/storage/serializers/NativeSerializerTest.java + src/main/java/org/exist/storage/serializers/NodeValueInputSource.java src/main/java/org/exist/util/ByteOrderMark.java src/main/java/org/exist/util/JREUtil.java src/main/java/org/exist/util/OSUtil.java @@ -783,10 +786,11 @@ src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java src/test/java/org/exist/xquery/functions/securitymanager/AccountMetadataFunctionsTest.java src/test/java/org/exist/xquery/functions/securitymanager/SecurityManagerTestUtil.java + src/main/java/org/exist/xquery/functions/system/FunctionAvailable.java src/test/java/org/exist/xquery/functions/system/GetMainModuleLoadPathTest.java src/main/java/org/exist/xquery/functions/system/GetModuleLoadPath.java src/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.java src/test/java/org/exist/xquery/functions/xmldb/XMLDBStoreTest.java src/test/java/org/exist/xquery/functions/xquery3/SerializeTest.java src/main/java/org/exist/xquery/value/ArrayWrapper.java @@ -820,9 +824,11 @@ src/test/xquery/maps/maps.xqm src/test/xquery/numbers/format-numbers.xql src/test/xquery/util/util.xml + src/test/xquery/xinclude/xinclude.xml src/test/xquery/xquery3/parse-xml.xqm src/test/xquery/xquery3/serialize.xql src/test/xquery/xquery3/xml-to-json.xql + src/test/java/xquery/xquery3/XQuery3Tests.java src/main/java/org/exist/Indexer.java src/test/java/org/exist/Indexer2Test.java src/test/java/org/exist/Indexer3Test.java @@ -1376,6 +1382,7 @@ src/main/java/org/exist/xquery/functions/fn/FnInnerMost.java src/main/java/org/exist/xquery/functions/fn/FnModule.java src/main/java/org/exist/xquery/functions/fn/FnOuterMost.java + src/main/java/org/exist/xquery/functions/fn/FnTransform.java src/main/java/org/exist/xquery/functions/fn/FunAbs.java src/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.java src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java @@ -1424,6 +1431,7 @@ src/test/java/org/exist/xquery/functions/fn/FunLangTest.java src/main/java/org/exist/xquery/functions/fn/FunLast.java src/main/java/org/exist/xquery/functions/fn/FunLocalName.java + src/main/java/org/exist/xquery/functions/fn/FunMatches.java src/main/java/org/exist/xquery/functions/fn/FunMax.java src/main/java/org/exist/xquery/functions/fn/FunMin.java src/main/java/org/exist/xquery/functions/fn/FunName.java @@ -1477,9 +1485,11 @@ src/main/java/org/exist/xquery/functions/fn/QNameFunctions.java src/main/java/org/exist/xquery/functions/fn/transform/Convert.java src/main/java/org/exist/xquery/functions/fn/transform/Delivery.java + src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.java src/main/java/org/exist/xquery/functions/fn/transform/Options.java src/main/java/org/exist/xquery/functions/fn/transform/Transform.java src/main/java/org/exist/xquery/functions/fn/transform/TreeUtils.java + src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java src/test/java/org/exist/xquery/functions/inspect/InspectModuleTest.java src/main/java/org/exist/xquery/functions/integer/WordPicture.java src/main/java/org/exist/xquery/functions/map/MapExpr.java @@ -1505,8 +1515,8 @@ src/main/java/org/exist/xquery/functions/system/SystemModule.java src/main/java/org/exist/xquery/functions/system/TriggerSystemTask.java src/test/resources-filtered/org/exist/xquery/functions/transform/transform-from-pkg-test.conf.xml - src/main/java/org/exist/xquery/functions/transform/Transform.java src/test/java/org/exist/xquery/functions/transform/TransformFromPkgTest.java + src/main/java/org/exist/xquery/functions/transform/TransformModule.java src/test/java/org/exist/xquery/functions/transform/TransformTest.java src/test/java/org/exist/xquery/functions/util/Base64FunctionsTest.java src/test/java/org/exist/xquery/functions/util/BaseConverterTest.java @@ -1591,7 +1601,10 @@ src/main/java/org/exist/xquery/value/ValueSequence.java src/test/java/org/exist/xquery/value/YearMonthDurationTest.java src/main/java/org/exist/xquery/value/YearMonthDurationValue.java + src/main/java/org/exist/xslt/EXistDbInputSource.java src/main/java/org/exist/xslt/EXistURIResolver.java + src/main/java/org/exist/xslt/SaxonConfiguration.java + src/test/java/org/exist/xslt/SaxonConfigurationTest.java src/main/java/org/exist/xslt/XsltURIResolverHelper.java src/main/java/org/exist/xupdate/Append.java src/main/java/org/exist/xupdate/Conditional.java @@ -1647,6 +1660,7 @@ src/test/xquery/numbers/format-numbers.xql src/test/xquery/securitymanager/acl.xqm src/test/xquery/util/util.xml + src/test/xquery/xinclude/xinclude.xml src/test/xquery/xqsuite/xqsuite-assertions-dynamic.xqm src/test/xquery/xqsuite/xqsuite-assertions-inline.xqm src/test/xquery/xqsuite/xqsuite-assertions.resources.xqm.ignore @@ -1655,6 +1669,8 @@ src/test/xquery/xquery3/postfix-expr.xqm src/test/xquery/xquery3/serialize.xql src/test/xquery/xquery3/xml-to-json.xql + src/test/java/xquery/xquery3/XQuery3Tests.java + src/test/java/xquery/xquery3/transform/XQuery3TransformTests.java src/main/java/org/exist/Indexer.java src/test/java/org/exist/Indexer2Test.java src/test/java/org/exist/Indexer3Test.java @@ -2022,7 +2038,9 @@ src/test/java/org/exist/storage/lock/ProtectedModeTest.java src/main/java/org/exist/storage/recovery/RecoveryManager.java src/main/java/org/exist/storage/serializers/EXistOutputKeys.java + src/main/java/org/exist/storage/serializers/FeatureKeys.java src/test/java/org/exist/storage/serializers/NativeSerializerTest.java + src/main/java/org/exist/storage/serializers/NodeValueInputSource.java src/main/java/org/exist/storage/serializers/Serializer.java src/main/java/org/exist/storage/serializers/XIncludeFilter.java src/test/resources-filtered/org/exist/storage/statistics/conf.xml @@ -2320,6 +2338,7 @@ src/main/java/org/exist/xquery/functions/fn/FnInnerMost.java src/main/java/org/exist/xquery/functions/fn/FnModule.java src/main/java/org/exist/xquery/functions/fn/FnOuterMost.java + src/main/java/org/exist/xquery/functions/fn/FnTransform.java src/main/java/org/exist/xquery/functions/fn/FunAbs.java src/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.java src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java @@ -2369,6 +2388,7 @@ src/test/java/org/exist/xquery/functions/fn/FunLangTest.java src/main/java/org/exist/xquery/functions/fn/FunLast.java src/main/java/org/exist/xquery/functions/fn/FunLocalName.java + src/main/java/org/exist/xquery/functions/fn/FunMatches.java src/main/java/org/exist/xquery/functions/fn/FunMax.java src/main/java/org/exist/xquery/functions/fn/FunMin.java src/main/java/org/exist/xquery/functions/fn/FunName.java @@ -2426,9 +2446,11 @@ src/test/java/org/exist/xquery/functions/fn/transform/ConvertTest.java src/main/java/org/exist/xquery/functions/fn/transform/Delivery.java src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java + src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.java src/main/java/org/exist/xquery/functions/fn/transform/Options.java src/main/java/org/exist/xquery/functions/fn/transform/Transform.java src/main/java/org/exist/xquery/functions/fn/transform/TreeUtils.java + src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java src/test/java/org/exist/xquery/functions/inspect/InspectModuleTest.java src/main/java/org/exist/xquery/functions/integer/WordPicture.java src/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.xml src/main/java/org/exist/xquery/functions/transform/Transform.java src/test/java/org/exist/xquery/functions/transform/TransformFromPkgTest.java + src/main/java/org/exist/xquery/functions/transform/TransformModule.java src/test/java/org/exist/xquery/functions/transform/TransformTest.java src/test/java/org/exist/xquery/functions/util/Base64FunctionsTest.java src/test/java/org/exist/xquery/functions/util/BaseConverterTest.java @@ -2566,7 +2589,10 @@ src/main/java/org/exist/xquery/value/ValueSequence.java src/test/java/org/exist/xquery/value/YearMonthDurationTest.java src/main/java/org/exist/xquery/value/YearMonthDurationValue.java + src/main/java/org/exist/xslt/EXistDbInputSource.java src/main/java/org/exist/xslt/EXistURIResolver.java + src/main/java/org/exist/xslt/SaxonConfiguration.java + src/test/java/org/exist/xslt/SaxonConfigurationTest.java src/main/java/org/exist/xslt/XsltURIResolverHelper.java src/main/java/org/exist/xupdate/Append.java src/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/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 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 6e17cfcee7..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 @@ -58,16 +58,25 @@ 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; @@ -75,10 +84,12 @@ 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; @@ -113,134 +124,181 @@ public class Transform { 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); + + } finally { + callingExpression.getContext().getBroker().returnSerializer(xmlReader); + } + } else { + final XdmItem xdmItem = (XdmItem) toSaxon.of(globalContextItem); + xslt30Transformer.setGlobalContextItem(xdmItem); } - final Xslt30Transformer xslt30Transformer = xsltExecutable.load30(); - xslt30Transformer.setMessageListener(new XsltMessageListener(context.getBroker().getBrokerPool().getSaxonProcessor(), getLogger())); - - 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); @@ -254,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())); @@ -296,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(); } @@ -313,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()); } /** @@ -331,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 = ""; @@ -350,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); @@ -388,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."); } @@ -403,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 " + @@ -427,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")); @@ -467,10 +605,6 @@ 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. * 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("" + + "

Start Template 1

" + + "

Start Template 2

" + + "

Template 3

" + + "

End Template 2

" + + "

Template 3

" + + "

End Template 1

" + + "
", result); + } + + /** + * Tests relative path resolution when parsing stylesheets in + * the transform:transform function. + */ + @Test + public void elementTransform() throws XMLDBException { + final String query = "import module namespace transform='http://exist-db.org/xquery/transform';\n" + "let $xml := \n" + - "let $xsl := 'xmldb:exist:///db/"+TEST_COLLECTION_NAME+"/xsl1/1.xsl'\n" + + "let $xsl := 'xmldb:exist:///db/" + TEST_COLLECTION_NAME + "/xsl1/1.xsl'\n" + "return transform:transform($xml, $xsl, ())"; - String result = execQuery(query); + final String result = execQuery(query); assertEquals("" + "

Start Template 1

" + "

Start Template 2

" + @@ -97,7 +120,18 @@ public void transform() throws XMLDBException { } @Test - public void transformWithSameDirectoryImportViaDbLocation() throws XMLDBException { + public void documentSameDirectoryImportViaDbLocation() throws XMLDBException { + final String query = + "import module namespace transform='http://exist-db.org/xquery/transform';\n" + + "let $xml := document { }\n" + + "let $xsl := '/db/" + TEST_COLLECTION_NAME + "/same-dir/a.xsl'\n" + + "return transform:transform($xml, $xsl, ())"; + final String result = execQuery(query); + assertEquals("

From A

From B

", result); + } + + @Test + public void elementSameDirectoryImportViaDbLocation() throws XMLDBException { final String query = "import module namespace transform='http://exist-db.org/xquery/transform';\n" + "let $xml := \n" + @@ -108,7 +142,18 @@ public void transformWithSameDirectoryImportViaDbLocation() throws XMLDBExceptio } @Test - public void transformWithSameDirectoryImportViaXmldbLocation() throws XMLDBException { + public void documentSameDirectoryImportViaXmldbLocation() 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 + "/same-dir/a.xsl'\n" + + "return transform:transform($xml, $xsl, ())"; + final String result = execQuery(query); + assertEquals("

From A

From B

", result); + } + + @Test + public void elementSameDirectoryImportViaXmldbLocation() throws XMLDBException { final String query = "import module namespace transform='http://exist-db.org/xquery/transform';\n" + "let $xml := \n" + @@ -119,7 +164,18 @@ public void transformWithSameDirectoryImportViaXmldbLocation() throws XMLDBExcep } @Test - public void transformWithSameDirectoryImportViaDbNode() throws XMLDBException { + public void documentSameDirectoryImportViaDbNode() throws XMLDBException { + final String query = + "import module namespace transform='http://exist-db.org/xquery/transform';\n" + + "let $xml := document { }\n" + + "let $xsl := doc('/db/" + TEST_COLLECTION_NAME + "/same-dir/a.xsl')\n" + + "return transform:transform($xml, $xsl, ())"; + final String result = execQuery(query); + assertEquals("

From A

From B

", result); + } + + @Test + public void elementSameDirectoryImportViaDbNode() throws XMLDBException { final String query = "import module namespace transform='http://exist-db.org/xquery/transform';\n" + "let $xml := \n" + @@ -130,7 +186,18 @@ public void transformWithSameDirectoryImportViaDbNode() throws XMLDBException { } @Test - public void transformWithSameDirectoryImportViaXmldbNode() throws XMLDBException { + public void documentSameDirectoryImportViaXmldbNode() throws XMLDBException { + final String query = + "import module namespace transform='http://exist-db.org/xquery/transform';\n" + + "let $xml := document { }\n" + + "let $xsl := doc('xmldb:exist:///db/" + TEST_COLLECTION_NAME + "/same-dir/a.xsl')\n" + + "return transform:transform($xml, $xsl, ())"; + final String result = execQuery(query); + assertEquals("

From A

From B

", result); + } + + @Test + public void elementSameDirectoryImportViaXmldbNode() throws XMLDBException { final String query = "import module namespace transform='http://exist-db.org/xquery/transform';\n" + "let $xml := \n" + @@ -160,6 +227,14 @@ private void addXMLDocument(final Collection c, final String doc, final String i } } + /*** + * Stores the following XSLT documents into the database: + * /db/transform-test/xsl1/1.xsl + * /db/transform-test/xsl3/3.xsl + * /db/transform-test/xsl1/xsl2/2.xsl + * /db/transform-test/same-dir/a.xsl + * /db/transform-test/same-dir/b.xsl + */ @Before public void setUp() throws ClassNotFoundException, IllegalAccessException, InstantiationException, XMLDBException { CollectionManagementService service = existEmbeddedServer.getRoot().getService(CollectionManagementService.class); @@ -171,7 +246,6 @@ public void setUp() throws ClassNotFoundException, IllegalAccessException, Insta try (final Collection xsl1 = service.createCollection("xsl1")) { assertNotNull(xsl1); - try (final Collection xsl3 = service.createCollection("xsl3")) { assertNotNull(xsl3); @@ -227,7 +301,7 @@ public void setUp() throws ClassNotFoundException, IllegalAccessException, Insta String docA = "\n" + "\n"+ "\n" + - "" + + "" + "

From A

" + "
" + "
"; diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/ConvertTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/ConvertTest.java index 99c427146d..366fbb57b0 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/ConvertTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/ConvertTest.java @@ -65,12 +65,7 @@ public class ConvertTest { private static final Configuration SAXON_CONFIGURATION = new Configuration(); private static final Processor SAXON_PROCESSOR = new Processor(SAXON_CONFIGURATION); - static final Convert.ToSaxon toSaxon = new Convert.ToSaxon() { - @Override - DocumentBuilder newDocumentBuilder() { - return SAXON_PROCESSOR.newDocumentBuilder(); - } - }; + static final Convert.ToSaxon toSaxon = new Convert.ToSaxon(SAXON_PROCESSOR); @Test public void memtreeDocumentToSaxon() throws XPathException { diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java index ea285e5ce1..15c7b2b6e5 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java @@ -51,14 +51,11 @@ import javax.xml.transform.Source; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.newCapture; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; @@ -76,7 +73,7 @@ public class FunTransformITTest { private static final String IMPORT_A_XSLT = "\n" + " \n" + - " \n" + + " \n" + "

From A

\n" + "
\n" + "
"; @@ -86,30 +83,54 @@ public class FunTransformITTest { "

From B

\n" + ""; - private static final String SAME_DIR_IMPORT_VIA_DB_LOCATION_QUERY = + private static final String DOCUMENT_SAME_DIR_IMPORT_VIA_DB_LOCATION_QUERY = "fn:transform(map {\n" + " \"stylesheet-location\": \"/db/fn-transform-import-test/a.xsl\",\n" + " \"source-node\": document { }\n" + "})?output"; - private static final String SAME_DIR_IMPORT_VIA_XMLDB_LOCATION_QUERY = + private static final String ELEMENT_SAME_DIR_IMPORT_VIA_DB_LOCATION_QUERY = + "fn:transform(map {\n" + + " \"stylesheet-location\": \"/db/fn-transform-import-test/a.xsl\",\n" + + " \"source-node\": \n" + + "})?output"; + + private static final String DOCUMENT_SAME_DIR_IMPORT_VIA_XMLDB_LOCATION_QUERY = "fn:transform(map {\n" + " \"stylesheet-location\": \"xmldb:exist:///db/fn-transform-import-test/a.xsl\",\n" + " \"source-node\": document { }\n" + "})?output"; - private static final String SAME_DIR_IMPORT_VIA_DB_NODE_QUERY = + private static final String ELEMENT_SAME_DIR_IMPORT_VIA_XMLDB_LOCATION_QUERY = + "fn:transform(map {\n" + + " \"stylesheet-location\": \"xmldb:exist:///db/fn-transform-import-test/a.xsl\",\n" + + " \"source-node\": \n" + + "})?output"; + + private static final String DOCUMENT_SAME_DIR_IMPORT_VIA_DB_NODE_QUERY = "fn:transform(map {\n" + " \"stylesheet-node\": doc(\"/db/fn-transform-import-test/a.xsl\"),\n" + " \"source-node\": document { }\n" + "})?output"; - private static final String SAME_DIR_IMPORT_VIA_XMLDB_NODE_QUERY = + private static final String ELEMENT_SAME_DIR_IMPORT_VIA_DB_NODE_QUERY = + "fn:transform(map {\n" + + " \"stylesheet-node\": doc(\"/db/fn-transform-import-test/a.xsl\"),\n" + + " \"source-node\": \n" + + "})?output"; + + private static final String DOCUMENT_SAME_DIR_IMPORT_VIA_XMLDB_NODE_QUERY = "fn:transform(map {\n" + " \"stylesheet-node\": doc(\"xmldb:exist:///db/fn-transform-import-test/a.xsl\"),\n" + " \"source-node\": document { }\n" + "})?output"; + private static final String ELEMENT_SAME_DIR_IMPORT_VIA_XMLDB_NODE_QUERY = + "fn:transform(map {\n" + + " \"stylesheet-node\": doc(\"xmldb:exist:///db/fn-transform-import-test/a.xsl\"),\n" + + " \"source-node\": \n" + + "})?output"; + private static final XmldbURI TEST_IDENTITY_XSLT_COLLECTION = XmldbURI.create("/db/transform-identity-test"); private static final XmldbURI IDENTITY_XSLT_NAME = XmldbURI.create("xsl-identity.xslt"); @@ -196,27 +217,51 @@ public class FunTransformITTest { public static ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true); @Test - public void sameDirectoryImportViaDbLocation() throws XPathException, PermissionDeniedException, EXistException, IOException { + public void documentSameDirectoryImportViaDbLocation() throws XPathException, PermissionDeniedException, EXistException, IOException { + final Source expected = Input.fromString("

From A

From B

").build(); + expectQuery(DOCUMENT_SAME_DIR_IMPORT_VIA_DB_LOCATION_QUERY, expected); + } + + @Test + public void elementSameDirectoryImportViaDbLocation() throws XPathException, PermissionDeniedException, EXistException, IOException { + final Source expected = Input.fromString("

From A

From B

").build(); + expectQuery(ELEMENT_SAME_DIR_IMPORT_VIA_DB_LOCATION_QUERY, expected); + } + + @Test + public void documentSameDirectoryImportViaXmldbLocation() throws XPathException, PermissionDeniedException, EXistException, IOException { + final Source expected = Input.fromString("

From A

From B

").build(); + expectQuery(DOCUMENT_SAME_DIR_IMPORT_VIA_XMLDB_LOCATION_QUERY, expected); + } + + @Test + public void elementSameDirectoryImportViaXmldbLocation() throws XPathException, PermissionDeniedException, EXistException, IOException { + final Source expected = Input.fromString("

From A

From B

").build(); + expectQuery(ELEMENT_SAME_DIR_IMPORT_VIA_XMLDB_LOCATION_QUERY, expected); + } + + @Test + public void documentSameDirectoryImportViaDbNode() throws XPathException, PermissionDeniedException, EXistException, IOException { final Source expected = Input.fromString("

From A

From B

").build(); - expectQuery(SAME_DIR_IMPORT_VIA_DB_LOCATION_QUERY, expected); + expectQuery(DOCUMENT_SAME_DIR_IMPORT_VIA_DB_NODE_QUERY, expected); } @Test - public void sameDirectoryImportViaXmldbLocation() throws XPathException, PermissionDeniedException, EXistException, IOException { + public void elementSameDirectoryImportViaDbNode() throws XPathException, PermissionDeniedException, EXistException, IOException { final Source expected = Input.fromString("

From A

From B

").build(); - expectQuery(SAME_DIR_IMPORT_VIA_XMLDB_LOCATION_QUERY, expected); + expectQuery(ELEMENT_SAME_DIR_IMPORT_VIA_DB_NODE_QUERY, expected); } @Test - public void sameDirectoryImportViaDbNode() throws XPathException, PermissionDeniedException, EXistException, IOException { + public void documentSameDirectoryImportViaXmldbNode() throws XPathException, PermissionDeniedException, EXistException, IOException { final Source expected = Input.fromString("

From A

From B

").build(); - expectQuery(SAME_DIR_IMPORT_VIA_DB_NODE_QUERY, expected); + expectQuery(DOCUMENT_SAME_DIR_IMPORT_VIA_XMLDB_NODE_QUERY, expected); } @Test - public void sameDirectoryImportViaXmldbNode() throws XPathException, PermissionDeniedException, EXistException, IOException { + public void elementSameDirectoryImportViaXmldbNode() throws XPathException, PermissionDeniedException, EXistException, IOException { final Source expected = Input.fromString("

From A

From B

").build(); - expectQuery(SAME_DIR_IMPORT_VIA_XMLDB_NODE_QUERY, expected); + expectQuery(ELEMENT_SAME_DIR_IMPORT_VIA_XMLDB_NODE_QUERY, expected); } @Test 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 @@