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..c0fb097e49 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,7 @@
import net.sf.saxon.s9api.*;
import net.sf.saxon.serialize.SerializationProperties;
import net.sf.saxon.trans.UncheckedXPathException;
+import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.dom.QName;
@@ -162,6 +163,34 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
final Xslt30Transformer xslt30Transformer = xsltExecutable.load30();
+ xslt30Transformer.setMessageListener((content, terminate, locator) ->{
+ try {
+ final StringBuilderWriter writer = new StringBuilderWriter();
+ final Serializer serializer = context.getBroker().getBrokerPool().getSaxonProcessor().newSerializer();
+ serializer.setOutputProperty(Serializer.Property.OMIT_XML_DECLARATION, "yes");
+ serializer.setOutputWriter(writer);
+ serializer.serializeNode(content);
+
+ 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;
+ }
+
+ LOGGER.info("{}",
+ terminate, source, sourceLine, sourceColumn, writer.toString());
+ } catch (final SaxonApiException e) {
+ LOGGER.error("Unable to serialize xsl:message content", e);
+ }
+ });
+
options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName())));
xslt30Transformer.setInitialTemplateParameters(options.templateParams, false);
xslt30Transformer.setInitialTemplateParameters(options.tunnelParams, true);
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..7afcab097f 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,7 +55,9 @@
import javax.xml.transform.Source;
import java.io.IOException;
+import java.util.List;
import java.util.Optional;
+import java.util.concurrent.CopyOnWriteArrayList;
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
import static org.junit.Assert.*;
@@ -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 {
+ 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" +
+ " ',\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);
+ } catch (final XPathException e) {
+ fail("Transform should have succeeded: " + e.getMessage());
+ }
+
+ final Optional logged = appender.getMessages().stream()
+ .filter(message -> message.contains(" messages = new CopyOnWriteArrayList<>();
+
+ 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;
+ }
+ }
}