1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 * <!DOCTYPE log4j:eventSet [<!ENTITY data SYSTEM "data.xml">]>
71 *
72 * <log4j:eventSet xmlns:log4j="http://jakarta.apache.org/log4j/">
73 * &data
74 * </log4j:eventSet>
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
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
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
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
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
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
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
379
380
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
411
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
444
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
474
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 }