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..8bb0a9fcb9 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;
@@ -161,6 +166,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
}
final Xslt30Transformer xslt30Transformer = xsltExecutable.load30();
+ xslt30Transformer.setMessageListener(new XsltMessageListener(context.getBroker().getBrokerPool().getSaxonProcessor()));
options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName())));
xslt30Transformer.setInitialTemplateParameters(options.templateParams, false);
@@ -532,4 +538,53 @@ public PendingException(String message, Throwable cause) {
super(message, cause);
}
}
+
+ private static class XsltMessageListener implements MessageListener {
+
+ private final Processor processor;
+
+ public XsltMessageListener(final Processor processor) {
+ this.processor = processor;
+ }
+
+ @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, 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..9e8e4ea70d 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,12 @@
package org.exist.xquery.functions.fn.transform;
import com.evolvedbinary.j8fu.tuple.Tuple2;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.layout.PatternLayout;
import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.security.PermissionDeniedException;
@@ -49,6 +55,8 @@
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;
@@ -247,6 +255,45 @@ public void identityMixedMemoryAndPersistentDom() throws XPathException, Permiss
expectQuery(IDENTITY_MIXED_XSLT_QUERY_5, expected);
}
+ @Test
+ public void xslMessageIsLogged() throws EXistException, PermissionDeniedException, IOException, XPathException {
+ final CapturingAppender appender = new CapturingAppender();
+ appender.start();
+
+ final Logger transformLogger = (Logger) LogManager.getLogger(Transform.class);
+ transformLogger.addAppender(appender);
+
+ try {
+ 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);
+ }
+
+ String logged = null;
+ for (final String message : appender.getMessages()) {
+ if (message.contains(" messages = new ArrayList<>();
+
+ CapturingAppender() {
+ super("capturing-appender", null, PatternLayout.createDefaultLayout(), false, Property.EMPTY_ARRAY);
+ }
+
+ @Override
+ public void append(final LogEvent event) {
+ messages.add(event.getMessage().getFormattedMessage());
+ }
+
+ List getMessages() {
+ return messages;
+ }
+ }
}