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