1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 * <!DOCTYPE log4j:eventSet [<!ENTITY data SYSTEM "data.xml">]>
67 *
68 * <log4j:eventSet xmlns:log4j="http://jakarta.apache.org/log4j/">
69 * &data
70 * </log4j:eventSet>
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
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
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
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
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
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
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
375
376
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
407
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
440
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
470
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 }