/* * 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.extras; 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; /** * This class is identical to org.apache.log4j.xml.XSLTLayout * except for a change in package to aid in use with OSGi. * * * 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> * <!DOCTYPE log4j:eventSet [<!ENTITY data SYSTEM "data.xml">]> * * <log4j:eventSet xmlns:log4j="http://jakarta.apache.org/log4j/"> * &data * </log4j:eventSet> * * </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 org.apache.log4j.xml.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) { for(endDecl += 2; endDecl < body.length() && (body.charAt(endDecl) == '\n' || body.charAt(endDecl) == '\r'); 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; } }