blob: 4c8a9c39e5d7033206d243e90da0b8ee2af786b1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.log4j.xml;
import org.apache.log4j.Layout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.MDCKeySetExtractor;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.LocationInfo;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.util.Set;
import java.util.Properties;
import java.util.Arrays;
import java.util.TimeZone;
import java.nio.charset.Charset;
import java.nio.ByteBuffer;
import org.apache.log4j.pattern.CachedDateFormat;
import java.text.SimpleDateFormat;
import org.w3c.dom.Document;
import org.xml.sax.helpers.AttributesImpl;
/**
* XSLTLayout transforms each event as a document using
* a specified or default XSLT transform. The default
* XSLT transform produces a result similar to XMLLayout.
*
* When used with a FileAppender or similar, the transformation of
* an event will be appended to the results for previous
* transforms. If each transform results in an XML element, then
* resulting file will only be an XML entity
* since an XML document requires one and only one top-level element.
* To process the entity, reference it in a XML document like so:
*
* <pre>
* &lt;!DOCTYPE log4j:eventSet [&lt;!ENTITY data SYSTEM &quot;data.xml&quot;&gt;]&gt;
*
* &lt;log4j:eventSet xmlns:log4j=&quot;http://jakarta.apache.org/log4j/&quot;&gt;
* &amp;data
* &lt;/log4j:eventSet&gt;
*
* </pre>
*
* The layout will detect the encoding and media-type specified in
* the transform. If no encoding is specified in the transform,
* an xsl:output element specifying the US-ASCII encoding will be inserted
* before processing the transform. If an encoding is specified in the transform,
* the same encoding should be explicitly specified for the appender.
*
* Extracting MDC values can be expensive when used with log4j releases
* prior to 1.2.15. Output of MDC values is enabled by default
* but be suppressed by setting properties to false.
*
* Extracting location info can be expensive regardless of log4j version.
* Output of location info is disabled by default but can be enabled
* by setting locationInfo to true.
*
* Embedded transforms in XML configuration should not
* depend on namespace prefixes defined earlier in the document
* as namespace aware parsing in not generally performed when
* using DOMConfigurator. The transform will serialize
* and reparse to get the namespace aware document needed.
*
*/
public final class XSLTLayout extends Layout
implements UnrecognizedElementHandler {
/**
* Namespace for XSLT.
*/
private static final String XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
/**
* Namespace for log4j events.
*/
private static final String LOG4J_NS = "http://jakarta.apache.org/log4j/";
/**
* Whether location information should be written.
*/
private boolean locationInfo = false;
/**
* media-type (mime type) extracted from XSLT transform.
*/
private String mediaType = "text/plain";
/**
* Encoding extracted from XSLT transform.
*/
private Charset encoding;
/**
* Transformer factory.
*/
private SAXTransformerFactory transformerFactory;
/**
* XSLT templates.
*/
private Templates templates;
/**
* Output stream.
*/
private final ByteArrayOutputStream outputStream;
/**
* Whether throwable information should be ignored.
*/
private boolean ignoresThrowable = false;
/**
* Whether properties should be extracted.
*/
private boolean properties = true;
/**
* Whether activateOptions has been called.
*/
private boolean activated = false;
/**
* DateFormat for UTC time.
*/
private final CachedDateFormat utcDateFormat;
/**
* Default constructor.
*
*/
public XSLTLayout() {
outputStream = new ByteArrayOutputStream();
transformerFactory = (SAXTransformerFactory)
TransformerFactory.newInstance();
SimpleDateFormat zdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
zdf.setTimeZone(TimeZone.getTimeZone("UTC"));
utcDateFormat = new CachedDateFormat(zdf, 1000);
}
/**
* {@inheritDoc}
*/
public synchronized String getContentType() {
return mediaType;
}
/**
* The <b>LocationInfo </b> option takes a boolean value. By default, it is
* set to false which means there will be no location information output by
* this layout. If the the option is set to true, then the file name and line
* number of the statement at the origin of the log statement will be output.
*
* <p>
* If you are embedding this layout within an {@link
* org.apache.log4j.net.SMTPAppender} then make sure to set the
* <b>LocationInfo </b> option of that appender as well.
*
* @param flag new value.
*/
public synchronized void setLocationInfo(final boolean flag) {
locationInfo = flag;
}
/**
* Gets whether location info should be output.
* @return if location is output.
*/
public synchronized boolean getLocationInfo() {
return locationInfo;
}
/**
* Sets whether MDC key-value pairs should be output, default false.
* @param flag new value.
*/
public synchronized void setProperties(final boolean flag) {
properties = flag;
}
/**
* Gets whether MDC key-value pairs should be output.
* @return true if MDC key-value pairs are output.
*/
public synchronized boolean getProperties() {
return properties;
}
/** {@inheritDoc} */
public synchronized void activateOptions() {
if (templates == null) {
try {
InputStream is = XSLTLayout.class.getResourceAsStream("default.xslt");
StreamSource ss = new StreamSource(is);
templates = transformerFactory.newTemplates(ss);
encoding = Charset.forName("US-ASCII");
mediaType = "text/plain";
} catch (Exception ex) {
LogLog.error("Error loading default.xslt", ex);
}
}
activated = true;
}
/**
* Gets whether throwables should not be output.
* @return true if throwables should not be output.
*/
public synchronized boolean ignoresThrowable() {
return ignoresThrowable;
}
/**
* Sets whether throwables should not be output.
* @param ignoresThrowable if true, throwables should not be output.
*/
public synchronized void setIgnoresThrowable(boolean ignoresThrowable) {
this.ignoresThrowable = ignoresThrowable;
}
/**
* {@inheritDoc}
*/
public synchronized String format(final LoggingEvent event) {
if (!activated) {
activateOptions();
}
if (templates != null && encoding != null) {
outputStream.reset();
try {
TransformerHandler transformer =
transformerFactory.newTransformerHandler(templates);
transformer.setResult(new StreamResult(outputStream));
transformer.startDocument();
//
// event element
//
AttributesImpl attrs = new AttributesImpl();
attrs.addAttribute(null, "logger", "logger",
"CDATA", event.getLoggerName());
attrs.addAttribute(null, "timestamp", "timestamp",
"CDATA", Long.toString(event.timeStamp));
attrs.addAttribute(null, "level", "level",
"CDATA", event.getLevel().toString());
attrs.addAttribute(null, "thread", "thread",
"CDATA", event.getThreadName());
StringBuffer buf = new StringBuffer();
utcDateFormat.format(event.timeStamp, buf);
attrs.addAttribute(null, "time", "time", "CDATA", buf.toString());
transformer.startElement(LOG4J_NS, "event", "event", attrs);
attrs.clear();
//
// message element
//
transformer.startElement(LOG4J_NS, "message", "message", attrs);
String msg = event.getRenderedMessage();
if (msg != null && msg.length() > 0) {
transformer.characters(msg.toCharArray(), 0, msg.length());
}
transformer.endElement(LOG4J_NS, "message", "message");
//
// NDC element
//
String ndc = event.getNDC();
if (ndc != null) {
transformer.startElement(LOG4J_NS, "NDC", "NDC", attrs);
char[] ndcChars = ndc.toCharArray();
transformer.characters(ndcChars, 0, ndcChars.length);
transformer.endElement(LOG4J_NS, "NDC", "NDC");
}
//
// throwable element unless suppressed
//
if (!ignoresThrowable) {
String[] s = event.getThrowableStrRep();
if (s != null) {
transformer.startElement(LOG4J_NS, "throwable",
"throwable", attrs);
char[] nl = new char[] { '\n' };
for (int i = 0; i < s.length; i++) {
char[] line = s[i].toCharArray();
transformer.characters(line, 0, line.length);
transformer.characters(nl, 0, nl.length);
}
transformer.endElement(LOG4J_NS, "throwable", "throwable");
}
}
//
// location info unless suppressed
//
//
if (locationInfo) {
LocationInfo locationInfo = event.getLocationInformation();
attrs.addAttribute(null, "class", "class", "CDATA",
locationInfo.getClassName());
attrs.addAttribute(null, "method", "method", "CDATA",
locationInfo.getMethodName());
attrs.addAttribute(null, "file", "file", "CDATA",
locationInfo.getFileName());
attrs.addAttribute(null, "line", "line", "CDATA",
locationInfo.getLineNumber());
transformer.startElement(LOG4J_NS, "locationInfo",
"locationInfo", attrs);
transformer.endElement(LOG4J_NS, "locationInfo",
"locationInfo");
}
if (properties) {
//
// write MDC contents out as properties element
//
Set mdcKeySet = MDCKeySetExtractor.INSTANCE.getPropertyKeySet(event);
if ((mdcKeySet != null) && (mdcKeySet.size() > 0)) {
attrs.clear();
transformer.startElement(LOG4J_NS,
"properties", "properties", attrs);
Object[] keys = mdcKeySet.toArray();
Arrays.sort(keys);
for (int i = 0; i < keys.length; i++) {
String key = keys[i].toString();
Object val = event.getMDC(key);
attrs.clear();
attrs.addAttribute(null, "name", "name", "CDATA", key);
attrs.addAttribute(null, "value", "value",
"CDATA", val.toString());
transformer.startElement(LOG4J_NS,
"data", "data", attrs);
transformer.endElement(LOG4J_NS, "data", "data");
}
}
}
transformer.endElement(LOG4J_NS, "event", "event");
transformer.endDocument();
String body = encoding.decode(
ByteBuffer.wrap(outputStream.toByteArray())).toString();
outputStream.reset();
//
// must remove XML declaration since it may
// result in erroneous encoding info
// if written by FileAppender in a different encoding
if (body.startsWith("<?xml ")) {
int endDecl = body.indexOf("?>");
if (endDecl != -1) {
endDecl += 2;
if (endDecl < body.length() && body.charAt(endDecl) == '\n') {
endDecl++;
}
return body.substring(endDecl);
}
}
return body;
} catch (Exception ex) {
LogLog.error("Error during transformation", ex);
return ex.toString();
}
}
return "No valid transform or encoding specified.";
}
/**
* Sets XSLT transform.
* @param xsltdoc DOM document containing XSLT transform source,
* may be modified.
* @throws TransformerConfigurationException if transformer can not be
* created.
*/
public void setTransform(final Document xsltdoc)
throws TransformerConfigurationException {
//
// scan transform source for xsl:output elements
// and extract encoding, media (mime) type and output method
//
String encodingName = null;
mediaType = null;
String method = null;
NodeList nodes = xsltdoc.getElementsByTagNameNS(
XSLT_NS,
"output");
for(int i = 0; i < nodes.getLength(); i++) {
Element outputElement = (Element) nodes.item(i);
if (method == null || method.length() == 0) {
method = outputElement.getAttributeNS(null, "method");
}
if (encodingName == null || encodingName.length() == 0) {
encodingName = outputElement.getAttributeNS(null, "encoding");
}
if (mediaType == null || mediaType.length() == 0) {
mediaType = outputElement.getAttributeNS(null, "media-type");
}
}
if (mediaType == null || mediaType.length() == 0) {
if ("html".equals(method)) {
mediaType = "text/html";
} else if ("xml".equals(method)) {
mediaType = "text/xml";
} else {
mediaType = "text/plain";
}
}
//
// if encoding was not specified,
// add xsl:output encoding=US-ASCII to XSLT source
//
if (encodingName == null || encodingName.length() == 0) {
Element transformElement = xsltdoc.getDocumentElement();
Element outputElement = xsltdoc.
createElementNS(XSLT_NS, "output");
outputElement.setAttributeNS(null, "encoding", "US-ASCII");
transformElement.insertBefore(outputElement, transformElement.getFirstChild());
encoding = Charset.forName("US-ASCII");
} else {
encoding = Charset.forName(encodingName);
}
DOMSource transformSource = new DOMSource(xsltdoc);
templates = transformerFactory.newTemplates(transformSource);
}
/**
* {@inheritDoc}
*/
public boolean parseUnrecognizedElement(final Element element,
final Properties props)
throws Exception {
if (XSLT_NS.equals(element.getNamespaceURI()) ||
element.getNodeName().indexOf("transform") != -1 ||
element.getNodeName().indexOf("stylesheet") != -1) {
//
// DOMConfigurator typically not namespace aware
// serialize tree and reparse.
ByteArrayOutputStream os = new ByteArrayOutputStream();
DOMSource source = new DOMSource(element);
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.transform(source, new StreamResult(os));
ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
Document xsltdoc = domFactory.newDocumentBuilder().parse(is);
setTransform(xsltdoc);
return true;
}
return false;
}
}