View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.log4j.xml;
18  import org.apache.log4j.Layout;
19  import org.apache.log4j.helpers.LogLog;
20  import org.apache.log4j.helpers.MDCKeySetExtractor;
21  import org.apache.log4j.spi.LoggingEvent;
22  import org.apache.log4j.spi.LocationInfo;
23  import org.w3c.dom.Element;
24  import org.w3c.dom.NodeList;
25  
26  import javax.xml.transform.TransformerFactory;
27  import javax.xml.transform.TransformerConfigurationException;
28  import javax.xml.transform.Templates;
29  import javax.xml.transform.Transformer;
30  import javax.xml.transform.sax.TransformerHandler;
31  import javax.xml.transform.sax.SAXTransformerFactory;
32  import javax.xml.transform.stream.StreamSource;
33  import javax.xml.transform.stream.StreamResult;
34  import javax.xml.transform.dom.DOMSource;
35  import javax.xml.parsers.DocumentBuilderFactory;
36  import java.io.InputStream;
37  import java.io.ByteArrayOutputStream;
38  import java.io.ByteArrayInputStream;
39  import java.util.Set;
40  import java.util.Properties;
41  import java.util.Arrays;
42  import java.util.TimeZone;
43  import java.nio.charset.Charset;
44  import java.nio.ByteBuffer;
45  import org.apache.log4j.pattern.CachedDateFormat;
46  import java.text.SimpleDateFormat;
47  
48  import org.w3c.dom.Document;
49  
50  import org.xml.sax.helpers.AttributesImpl;
51  
52  
53  /***
54   * XSLTLayout transforms each event as a document using
55   * a specified or default XSLT transform.  The default
56   * XSLT transform produces a result similar to XMLLayout.
57   *
58   * When used with a FileAppender or similar, the transformation of
59   * an event will be appended to the results for previous
60   * transforms.  If each transform results in an XML element, then
61   * resulting file will only be an XML entity
62   * since an XML document requires one and only one top-level element.
63   * To process the entity, reference it in a XML document like so:
64   *
65   * <pre>
66   *  &lt;!DOCTYPE log4j:eventSet [&lt;!ENTITY data SYSTEM &quot;data.xml&quot;&gt;]&gt;
67   *
68   *  &lt;log4j:eventSet xmlns:log4j=&quot;http://jakarta.apache.org/log4j/&quot;&gt;
69   *    &amp;data
70   *  &lt;/log4j:eventSet&gt;
71   *
72   * </pre>
73   *
74   * The layout will detect the encoding and media-type specified in
75   * the transform.  If no encoding is specified in the transform,
76   * an xsl:output element specifying the US-ASCII encoding will be inserted
77   * before processing the transform.  If an encoding is specified in the transform,
78   * the same encoding should be explicitly specified for the appender.
79   *
80   * Extracting MDC values can be expensive when used with log4j releases
81   * prior to 1.2.15.  Output of MDC values is enabled by default
82   * but be suppressed by setting properties to false.
83   *
84   * Extracting location info can be expensive regardless of log4j version.  
85   * Output of location info is disabled by default but can be enabled
86   * by setting locationInfo to true.
87   *
88   * Embedded transforms in XML configuration should not
89   * depend on namespace prefixes defined earlier in the document
90   * as namespace aware parsing in not generally performed when
91   * using DOMConfigurator.  The transform will serialize
92   * and reparse to get the namespace aware document needed.
93   *
94   */
95  public final class XSLTLayout extends Layout
96          implements UnrecognizedElementHandler {
97      /***
98       * Namespace for XSLT.
99       */
100     private static final String XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
101     /***
102      * Namespace for log4j events.
103      */
104     private static final String LOG4J_NS = "http://jakarta.apache.org/log4j/";
105     /***
106      * Whether location information should be written.
107      */
108     private boolean locationInfo = false;
109     /***
110      * media-type (mime type) extracted from XSLT transform.
111      */
112     private String mediaType = "text/plain";
113     /***
114      * Encoding extracted from XSLT transform.
115      */
116     private Charset encoding;
117     /***
118      * Transformer factory.
119      */
120     private SAXTransformerFactory transformerFactory;
121     /***
122      * XSLT templates.
123      */
124     private Templates templates;
125     /***
126      * Output stream.
127      */
128     private final ByteArrayOutputStream outputStream;
129     /***
130      * Whether throwable information should be ignored.
131      */
132     private boolean ignoresThrowable = false;
133     /***
134      * Whether properties should be extracted.
135      */
136     private boolean properties = true;
137     /***
138      * Whether activateOptions has been called.
139      */
140     private boolean activated = false;
141 
142     /***
143      * DateFormat for UTC time.
144      */
145     private final CachedDateFormat utcDateFormat;
146 
147     /***
148      * Default constructor.
149      *
150      */
151     public XSLTLayout() {
152         outputStream = new ByteArrayOutputStream();
153         transformerFactory = (SAXTransformerFactory)
154                 TransformerFactory.newInstance();
155 
156         SimpleDateFormat zdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
157         zdf.setTimeZone(TimeZone.getTimeZone("UTC"));
158         utcDateFormat = new CachedDateFormat(zdf, 1000);
159     }
160 
161     /***
162      * {@inheritDoc}
163      */
164     public synchronized String getContentType() {
165         return mediaType;
166     }
167 
168     /***
169      * The <b>LocationInfo </b> option takes a boolean value. By default, it is
170      * set to false which means there will be no location information output by
171      * this layout. If the the option is set to true, then the file name and line
172      * number of the statement at the origin of the log statement will be output.
173      *
174      * <p>
175      * If you are embedding this layout within an {@link
176      * org.apache.log4j.net.SMTPAppender} then make sure to set the
177      * <b>LocationInfo </b> option of that appender as well.
178      *
179      * @param flag new value.
180      */
181     public synchronized void setLocationInfo(final boolean flag) {
182       locationInfo = flag;
183     }
184 
185     /***
186      * Gets whether location info should be output.
187      * @return if location is output.
188      */
189     public synchronized boolean getLocationInfo() {
190       return locationInfo;
191     }
192 
193     /***
194      * Sets whether MDC key-value pairs should be output, default false.
195      * @param flag new value.
196      */
197     public synchronized void setProperties(final boolean flag) {
198       properties = flag;
199     }
200 
201     /***
202      * Gets whether MDC key-value pairs should be output.
203      * @return true if MDC key-value pairs are output.
204      */
205     public synchronized boolean getProperties() {
206       return properties;
207     }
208 
209 
210     /*** {@inheritDoc} */
211     public synchronized void activateOptions() {
212         if (templates == null) {
213             try {
214                 InputStream is = XSLTLayout.class.getResourceAsStream("default.xslt");
215                 StreamSource ss = new StreamSource(is);
216                 templates = transformerFactory.newTemplates(ss);
217                 encoding = Charset.forName("US-ASCII");
218                 mediaType = "text/plain";
219             } catch (Exception ex) {
220                 LogLog.error("Error loading default.xslt", ex);
221             }
222         }
223         activated = true;
224     }
225 
226     /***
227      * Gets whether throwables should not be output.
228      * @return true if throwables should not be output.
229      */
230     public synchronized boolean ignoresThrowable() {
231         return ignoresThrowable;
232     }
233 
234     /***
235      * Sets whether throwables should not be output.
236      * @param ignoresThrowable if true, throwables should not be output.
237     */
238     public synchronized void setIgnoresThrowable(boolean ignoresThrowable) {
239       this.ignoresThrowable = ignoresThrowable;
240     }
241 
242 
243 
244     /***
245      * {@inheritDoc}
246      */
247     public synchronized String format(final LoggingEvent event) {
248       if (!activated) {
249           activateOptions();
250       }
251       if (templates != null && encoding != null) {
252           outputStream.reset();
253 
254           try {
255             TransformerHandler transformer =
256                       transformerFactory.newTransformerHandler(templates);
257 
258             transformer.setResult(new StreamResult(outputStream));
259             transformer.startDocument();
260 
261             //
262             //   event element
263             //
264             AttributesImpl attrs = new AttributesImpl();
265             attrs.addAttribute(null, "logger", "logger",
266                     "CDATA", event.getLoggerName());
267             attrs.addAttribute(null, "timestamp", "timestamp",
268                     "CDATA", Long.toString(event.timeStamp));
269             attrs.addAttribute(null, "level", "level",
270                     "CDATA", event.getLevel().toString());
271             attrs.addAttribute(null, "thread", "thread",
272                     "CDATA", event.getThreadName());
273             StringBuffer buf = new StringBuffer();
274             utcDateFormat.format(event.timeStamp, buf);
275             attrs.addAttribute(null, "time", "time", "CDATA", buf.toString());
276 
277 
278             transformer.startElement(LOG4J_NS, "event", "event", attrs);
279             attrs.clear();
280 
281             //
282             //   message element
283             //
284             transformer.startElement(LOG4J_NS, "message", "message", attrs);
285             String msg = event.getRenderedMessage();
286             if (msg != null && msg.length() > 0) {
287                 transformer.characters(msg.toCharArray(), 0, msg.length());
288             }
289             transformer.endElement(LOG4J_NS, "message", "message");
290 
291             //
292             //    NDC element
293             //
294             String ndc = event.getNDC();
295             if (ndc != null) {
296                 transformer.startElement(LOG4J_NS, "NDC", "NDC", attrs);
297                 char[] ndcChars = ndc.toCharArray();
298                 transformer.characters(ndcChars, 0, ndcChars.length);
299                 transformer.endElement(LOG4J_NS, "NDC", "NDC");
300             }
301 
302             //
303             //    throwable element unless suppressed
304             //
305               if (!ignoresThrowable) {
306                 String[] s = event.getThrowableStrRep();
307                 if (s != null) {
308                     transformer.startElement(LOG4J_NS, "throwable",
309                             "throwable", attrs);
310                     char[] nl = new char[] { '\n' };
311                     for (int i = 0; i < s.length; i++) {
312                         char[] line = s[i].toCharArray();
313                         transformer.characters(line, 0, line.length);
314                         transformer.characters(nl, 0, nl.length);
315                     }
316                     transformer.endElement(LOG4J_NS, "throwable", "throwable");
317                 }
318               }
319 
320               //
321               //     location info unless suppressed
322               //
323               //
324               if (locationInfo) {
325                 LocationInfo locationInfo = event.getLocationInformation();
326                 attrs.addAttribute(null, "class", "class", "CDATA",
327                         locationInfo.getClassName());
328                 attrs.addAttribute(null, "method", "method", "CDATA",
329                           locationInfo.getMethodName());
330                 attrs.addAttribute(null, "file", "file", "CDATA",
331                             locationInfo.getFileName());
332                 attrs.addAttribute(null, "line", "line", "CDATA",
333                             locationInfo.getLineNumber());
334                 transformer.startElement(LOG4J_NS, "locationInfo",
335                         "locationInfo", attrs);
336                 transformer.endElement(LOG4J_NS, "locationInfo",
337                         "locationInfo");
338               }
339 
340               if (properties) {
341                 //
342                 //    write MDC contents out as properties element
343                 //
344                 Set mdcKeySet = MDCKeySetExtractor.INSTANCE.getPropertyKeySet(event);
345 
346                 if ((mdcKeySet != null) && (mdcKeySet.size() > 0)) {
347                     attrs.clear();
348                     transformer.startElement(LOG4J_NS,
349                             "properties", "properties", attrs);
350                     Object[] keys = mdcKeySet.toArray();
351                     Arrays.sort(keys);
352                     for (int i = 0; i < keys.length; i++) {
353                         String key = keys[i].toString();
354                         Object val = event.getMDC(key);
355                         attrs.clear();
356                         attrs.addAttribute(null, "name", "name", "CDATA", key);
357                         attrs.addAttribute(null, "value", "value",
358                                 "CDATA", val.toString());
359                         transformer.startElement(LOG4J_NS,
360                                 "data", "data", attrs);
361                         transformer.endElement(LOG4J_NS, "data", "data");
362                     }
363                 }
364               }
365 
366 
367             transformer.endElement(LOG4J_NS, "event", "event");
368             transformer.endDocument();
369 
370             String body = encoding.decode(
371                     ByteBuffer.wrap(outputStream.toByteArray())).toString();
372             outputStream.reset();
373             //
374             //   must remove XML declaration since it may
375             //      result in erroneous encoding info
376             //      if written by FileAppender in a different encoding
377             if (body.startsWith("<?xml ")) {
378                 int endDecl = body.indexOf("?>");
379                 if (endDecl != -1) {
380                     for(endDecl += 2; 
381 					     endDecl < body.length() &&
382 						 (body.charAt(endDecl) == '\n' || body.charAt(endDecl) == '\r'); 
383 						 endDecl++);
384                     return body.substring(endDecl);
385                 }
386             }
387             return body;
388           } catch (Exception ex) {
389               LogLog.error("Error during transformation", ex);
390               return ex.toString();
391           }
392       }
393       return "No valid transform or encoding specified.";
394     }
395 
396     /***
397      * Sets XSLT transform.
398      * @param xsltdoc DOM document containing XSLT transform source,
399      * may be modified.
400      * @throws TransformerConfigurationException if transformer can not be
401      * created.
402      */
403     public void setTransform(final Document xsltdoc)
404             throws TransformerConfigurationException {
405         //
406         //  scan transform source for xsl:output elements
407         //    and extract encoding, media (mime) type and output method
408         //
409         String encodingName = null;
410         mediaType = null;
411         String method = null;
412         NodeList nodes = xsltdoc.getElementsByTagNameNS(
413                 XSLT_NS,
414                 "output");
415         for(int i = 0; i < nodes.getLength(); i++) {
416             Element outputElement = (Element) nodes.item(i);
417             if (method == null || method.length() == 0) {
418                 method = outputElement.getAttributeNS(null, "method");
419             }
420             if (encodingName == null || encodingName.length() == 0) {
421                 encodingName = outputElement.getAttributeNS(null, "encoding");
422             }
423             if (mediaType == null || mediaType.length() == 0) {
424                 mediaType = outputElement.getAttributeNS(null, "media-type");
425             }
426         }
427 
428         if (mediaType == null || mediaType.length() == 0) {
429             if ("html".equals(method)) {
430                 mediaType = "text/html";
431             } else if ("xml".equals(method)) {
432                 mediaType = "text/xml";
433             } else {
434                 mediaType = "text/plain";
435             }
436         }
437 
438         //
439         //  if encoding was not specified,
440         //     add xsl:output encoding=US-ASCII to XSLT source
441         //
442         if (encodingName == null || encodingName.length() == 0) {
443             Element transformElement = xsltdoc.getDocumentElement();
444             Element outputElement = xsltdoc.
445                     createElementNS(XSLT_NS, "output");
446             outputElement.setAttributeNS(null, "encoding", "US-ASCII");
447             transformElement.insertBefore(outputElement, transformElement.getFirstChild());
448             encoding = Charset.forName("US-ASCII");
449         } else {
450             encoding = Charset.forName(encodingName);
451         }
452 
453         DOMSource transformSource = new DOMSource(xsltdoc);
454         
455         templates = transformerFactory.newTemplates(transformSource);
456 
457     }
458 
459     /***
460      * {@inheritDoc}
461      */
462     public boolean parseUnrecognizedElement(final Element element,
463                                             final Properties props)
464             throws Exception {
465         if (XSLT_NS.equals(element.getNamespaceURI()) ||
466                 element.getNodeName().indexOf("transform") != -1 ||
467                 element.getNodeName().indexOf("stylesheet") != -1) {
468             //
469             //   DOMConfigurator typically not namespace aware
470             //     serialize tree and reparse.
471             ByteArrayOutputStream os = new ByteArrayOutputStream();
472             DOMSource source = new DOMSource(element);
473             TransformerFactory transformerFactory = TransformerFactory.newInstance();
474             Transformer transformer = transformerFactory.newTransformer();
475             transformer.transform(source, new StreamResult(os));
476 
477             ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
478             DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
479             domFactory.setNamespaceAware(true);
480             Document xsltdoc = domFactory.newDocumentBuilder().parse(is);
481             setTransform(xsltdoc);
482             return true;
483         }
484         return false;
485     }
486 
487 
488 }