Skip to content

Commit eb0becd

Browse files
committed
[bugfix] Log xsl:message output from fn:transform() to Elemental's log
Closes #234
1 parent 5aafd52 commit eb0becd

2 files changed

Lines changed: 126 additions & 1 deletion

File tree

‎exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
import net.sf.saxon.s9api.*;
5454
import net.sf.saxon.serialize.SerializationProperties;
5555
import net.sf.saxon.trans.UncheckedXPathException;
56+
57+
import java.io.IOException;
58+
import java.io.StringWriter;
5659
import org.apache.logging.log4j.LogManager;
5760
import org.apache.logging.log4j.Logger;
5861
import org.exist.dom.QName;
@@ -67,8 +70,10 @@
6770
import org.w3c.dom.Node;
6871

6972
import javax.annotation.Nonnull;
73+
import javax.annotation.Nullable;
7074
import javax.xml.transform.ErrorListener;
7175
import javax.xml.transform.Source;
76+
import javax.xml.transform.SourceLocator;
7277
import javax.xml.transform.TransformerException;
7378
import javax.xml.transform.dom.DOMSource;
7479
import java.net.URI;
@@ -79,6 +84,7 @@
7984

8085
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
8186
import static org.exist.util.StringUtil.isNullOrEmpty;
87+
import static org.exist.util.StringUtil.notNullOrEmpty;
8288
import static org.exist.xquery.functions.fn.transform.Options.Option.*;
8389

8490
/**
@@ -104,7 +110,7 @@
104110
*/
105111
public class Transform {
106112

107-
private static final Logger LOGGER = LogManager.getLogger(org.exist.xquery.functions.fn.transform.Transform.class);
113+
private static Logger LOGGER = LogManager.getLogger(org.exist.xquery.functions.fn.transform.Transform.class);
108114
private static final org.exist.xquery.functions.fn.transform.Transform.ErrorListenerLog4jAdapter ERROR_LISTENER = new Transform.ErrorListenerLog4jAdapter(Transform.LOGGER);
109115

110116
final Convert.ToSaxon toSaxon = new Convert.ToSaxon() {
@@ -161,6 +167,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
161167
}
162168

163169
final Xslt30Transformer xslt30Transformer = xsltExecutable.load30();
170+
xslt30Transformer.setMessageListener(new XsltMessageListener(context.getBroker().getBrokerPool().getSaxonProcessor(), getLogger()));
164171

165172
options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName())));
166173
xslt30Transformer.setInitialTemplateParameters(options.templateParams, false);
@@ -464,6 +471,19 @@ private static Optional<Source> getSourceNode(final Optional<NodeValue> sourceNo
464471
return sourceNode.map(NodeValue::getNode).map(node -> new DOMSource(node, baseURI.getStringValue()));
465472
}
466473

474+
/**
475+
* Designed to be package-protected accessible so that we can observe logging in tests.
476+
*
477+
* @param logger the logger to use in testing.
478+
*/
479+
static void setLogger(final Logger logger) {
480+
LOGGER = logger;
481+
}
482+
483+
private Logger getLogger() {
484+
return LOGGER;
485+
}
486+
467487
private static class ErrorListenerLog4jAdapter implements ErrorListener {
468488
private final Logger logger;
469489

@@ -532,4 +552,55 @@ public PendingException(String message, Throwable cause) {
532552
super(message, cause);
533553
}
534554
}
555+
556+
private static class XsltMessageListener implements MessageListener {
557+
558+
private final Processor processor;
559+
private final Logger logger;
560+
561+
public XsltMessageListener(final Processor processor, final Logger logger) {
562+
this.processor = processor;
563+
this.logger = logger;
564+
}
565+
566+
@Override
567+
public void message(final XdmNode content, final boolean terminate, final SourceLocator locator) {
568+
569+
try (final StringWriter writer = new StringWriter()) {
570+
final Serializer serializer = processor.newSerializer();
571+
serializer.setOutputProperty(Serializer.Property.OMIT_XML_DECLARATION, "yes");
572+
serializer.setOutputWriter(writer);
573+
serializer.serializeNode(content);
574+
575+
@Nullable final String source;
576+
final int sourceLine;
577+
final int sourceColumn;
578+
if (locator != null) {
579+
source = locator.getSystemId();
580+
sourceLine = locator.getLineNumber();
581+
sourceColumn = locator.getColumnNumber();
582+
} else {
583+
source = null;
584+
sourceLine = -1;
585+
sourceColumn = -1;
586+
}
587+
588+
final StringBuilder tag = new StringBuilder("<xsl:message terminate=\"" + terminate + "\"");
589+
if (notNullOrEmpty(source)) {
590+
tag.append(" source=\"").append(source).append("\"");
591+
}
592+
if (sourceLine != -1) {
593+
tag.append(" sourceLine=\"").append(sourceLine).append("\"");
594+
tag.append(" sourceColumn=\"").append(sourceColumn).append("\"");
595+
}
596+
tag.append(">");
597+
598+
logger.info("{}{}</xsl:message>", tag.toString(), writer.toString());
599+
} catch (final SaxonApiException e) {
600+
logger.error("Unable to serialize xsl:message content", e);
601+
} catch (final IOException e) {
602+
logger.error("Unable to close xsl:message writer", e);
603+
}
604+
}
605+
}
535606
}

‎exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
package org.exist.xquery.functions.fn.transform;
2222

2323
import com.evolvedbinary.j8fu.tuple.Tuple2;
24+
import org.apache.logging.log4j.Logger;
25+
import org.easymock.Capture;
2426
import org.exist.EXistException;
2527
import org.exist.collections.Collection;
2628
import org.exist.security.PermissionDeniedException;
@@ -49,9 +51,17 @@
4951

5052
import javax.xml.transform.Source;
5153
import java.io.IOException;
54+
import java.util.ArrayList;
55+
import java.util.List;
5256
import java.util.Optional;
5357

5458
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
59+
import static org.easymock.EasyMock.capture;
60+
import static org.easymock.EasyMock.createMock;
61+
import static org.easymock.EasyMock.expect;
62+
import static org.easymock.EasyMock.newCapture;
63+
import static org.easymock.EasyMock.replay;
64+
import static org.easymock.EasyMock.verify;
5565
import static org.junit.Assert.*;
5666

5767
/**
@@ -247,6 +257,50 @@ public void identityMixedMemoryAndPersistentDom() throws XPathException, Permiss
247257
expectQuery(IDENTITY_MIXED_XSLT_QUERY_5, expected);
248258
}
249259

260+
@Test
261+
public void xslMessageIsLogged() throws EXistException, PermissionDeniedException, IOException, XPathException {
262+
263+
// set a mock logger so we can capture the log output for our test
264+
final Logger mockLogger = createMock(Logger.class);
265+
Transform.setLogger(mockLogger);
266+
267+
// expectations
268+
final Capture<String> formatPattern = newCapture();
269+
final Capture<String> startTagCapture = newCapture();
270+
final Capture<String> logMessageCapture = newCapture();
271+
mockLogger.info(capture(formatPattern), capture(startTagCapture), capture(logMessageCapture));
272+
273+
// reset mock state before test
274+
replay(mockLogger);
275+
276+
// execute test
277+
final String query =
278+
"fn:transform(map {\n" +
279+
" \"stylesheet-text\": '<xsl:stylesheet xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" version=\"2.0\">\n" +
280+
" <xsl:template match=\"/\">\n" +
281+
" <xsl:message>Hello from XSLT</xsl:message>\n" +
282+
" </xsl:template>\n" +
283+
" </xsl:stylesheet>',\n" +
284+
" \"source-node\": document { <in/> }\n" +
285+
"})?output";
286+
287+
final BrokerPool pool = existEmbeddedServer.getBrokerPool();
288+
try (final DBBroker broker = pool.get(Optional.of(pool.getSecurityManager().getSystemSubject()));
289+
final XQueryUtil.QueryResult queryResult = XQueryUtil.query(broker, new StringSource(query), false, null, null, null, null, null)) {
290+
assertNotNull(queryResult.result);
291+
}
292+
293+
// verify our expectations
294+
verify(mockLogger);
295+
296+
// check our assertions about the log message
297+
final String startTag = startTagCapture.getValue();
298+
final String message = logMessageCapture.getValue();
299+
300+
assertEquals("<xsl:message terminate=\"false\" sourceLine=\"3\" sourceColumn=\"20\">", startTag);
301+
assertEquals("Hello from XSLT", message);
302+
}
303+
250304
private static void expectQuery(final String query, final Source expected) throws EXistException, XPathException, PermissionDeniedException, IOException {
251305
final BrokerPool pool = existEmbeddedServer.getBrokerPool();
252306
try(final DBBroker broker = pool.get(Optional.of(pool.getSecurityManager().getSystemSubject()));

0 commit comments

Comments
 (0)