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  
18  package org.apache.log4j.xml;
19  
20  import java.awt.Component;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.LineNumberReader;
25  import java.io.StringReader;
26  import java.net.URL;
27  import java.util.HashMap;
28  import java.util.Hashtable;
29  import java.util.Iterator;
30  import java.util.Map;
31  import java.util.Vector;
32  import java.util.zip.ZipInputStream;
33  
34  import javax.swing.ProgressMonitorInputStream;
35  import javax.xml.parsers.DocumentBuilder;
36  import javax.xml.parsers.DocumentBuilderFactory;
37  import javax.xml.parsers.ParserConfigurationException;
38  
39  import org.apache.log4j.Level;
40  import org.apache.log4j.Logger;
41  import org.apache.log4j.spi.Decoder;
42  import org.apache.log4j.spi.LocationInfo;
43  import org.apache.log4j.spi.LoggingEvent;
44  import org.apache.log4j.spi.ThrowableInformation;
45  import org.w3c.dom.Document;
46  import org.w3c.dom.Node;
47  import org.w3c.dom.NodeList;
48  import org.xml.sax.InputSource;
49  
50  
51  /**
52   * Decodes Logging Events in XML formated into elements that are used by
53   * Chainsaw.
54   *
55   * This decoder can process a collection of log4j:event nodes ONLY
56   * (no XML declaration nor eventSet node)
57   *
58   * NOTE:  Only a single LoggingEvent is returned from the decode method
59   * even though the DTD supports multiple events nested in an eventSet.
60   *
61   * NOTE: This class has been created on the assumption that all XML log files
62   * are encoded in UTF-8. There is no current support for any other
63   * encoding format at this time.
64   * 
65   * @author Scott Deboy (sdeboy@apache.org)
66   * @author Paul Smith (psmith@apache.org)
67   *
68   */
69  public class XMLDecoder implements Decoder {
70      
71    private static final String ENCODING = "UTF-8";
72      
73      /**
74       * Document prolog.
75       */
76    private static final String BEGINPART =
77      "<?xml version=\"1.0\" encoding=\"" + ENCODING + "\" ?>"
78      + "<!DOCTYPE log4j:eventSet SYSTEM \"http://localhost/log4j.dtd\">"
79      + "<log4j:eventSet version=\"1.2\" "
80      + "xmlns:log4j=\"http://jakarta.apache.org/log4j/\">";
81      /**
82       * Document close.
83       */
84    private static final String ENDPART = "</log4j:eventSet>";
85      /**
86       * Record end.
87       */
88    private static final String RECORD_END = "</log4j:event>";
89  
90      /**
91       * Document builder.
92       */
93    private DocumentBuilder docBuilder;
94      /**
95       * Additional properties.
96       */
97    private Map additionalProperties = new HashMap();
98      /**
99       * Partial event.
100      */
101   private String partialEvent;
102     /**
103      * Owner.
104      */
105   private Component owner = null;
106 
107     /**
108      * Create new instance.
109      * @param o owner
110      */
111   public XMLDecoder(final Component o) {
112       this();
113       this.owner = o;
114   }
115 
116     /**
117      * Create new instance.
118      */
119    public XMLDecoder() {
120     DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
121     dbf.setValidating(false);
122 
123     try {
124       docBuilder = dbf.newDocumentBuilder();
125       docBuilder.setErrorHandler(new SAXErrorHandler());
126       docBuilder.setEntityResolver(new Log4jEntityResolver());
127     } catch (ParserConfigurationException pce) {
128       System.err.println("Unable to get document builder");
129     }
130   }
131 
132   /**
133    * Sets an additionalProperty map, where each Key/Value pair is
134    * automatically added to each LoggingEvent as it is decoded.
135    *
136    * This is useful, say, to include the source file name of the Logging events
137    * @param properties additional properties
138    */
139   public void setAdditionalProperties(final Map properties) {
140     this.additionalProperties = properties;
141   }
142 
143   /**
144    * Converts the LoggingEvent data in XML string format into an actual
145    * XML Document class instance.
146    * @param data XML fragment
147    * @return dom document
148    */
149   private Document parse(final String data) {
150     if (docBuilder == null || data == null) {
151       return null;
152     }
153     Document document = null;
154 
155     try {
156       // we change the system ID to a valid URI so that Crimson won't
157       // complain. Indeed, "log4j.dtd" alone is not a valid URI which
158       // causes Crimson to barf. The Log4jEntityResolver only cares
159       // about the "log4j.dtd" ending.
160 
161       /**
162        * resetting the length of the StringBuffer is dangerous, particularly
163        * on some JDK 1.4 impls, there's a known Bug that causes a memory leak
164        */
165       StringBuffer buf = new StringBuffer(1024);
166 
167       buf.append(BEGINPART);
168       buf.append(data);
169       buf.append(ENDPART);
170 
171       InputSource inputSource =
172         new InputSource(new StringReader(buf.toString()));
173       document = docBuilder.parse(inputSource);
174     } catch (Exception e) {
175       e.printStackTrace();
176     }
177 
178     return document;
179   }
180 
181   /**
182    * Decodes a File into a Vector of LoggingEvents.
183    * @param url the url of a file containing events to decode
184    * @return Vector of LoggingEvents
185    * @throws IOException if IO error during processing.
186    */
187   public Vector decode(final URL url) throws IOException {
188     LineNumberReader reader;
189     boolean isZipFile = url.getPath().toLowerCase().endsWith(".zip");
190     InputStream inputStream;
191     if (isZipFile) {
192         inputStream = new ZipInputStream(url.openStream());
193         //move stream to next entry so we can read it
194         ((ZipInputStream)inputStream).getNextEntry();
195     } else {
196         inputStream = url.openStream();
197     }
198     if (owner != null) {
199         reader = new LineNumberReader(
200                 new InputStreamReader(
201                         new ProgressMonitorInputStream(owner,
202                                 "Loading " + url , inputStream), ENCODING));
203     } else {
204         reader = new LineNumberReader(new InputStreamReader(inputStream, ENCODING));
205     }
206 
207     Vector v = new Vector();
208 
209     String line;
210     Vector events;
211     try {
212         while ((line = reader.readLine()) != null) {
213             StringBuffer buffer = new StringBuffer(line);
214             for (int i = 0; i < 1000; i++) {
215                 buffer.append(reader.readLine()).append("\n");
216             }
217             events = decodeEvents(buffer.toString());
218             if (events != null) {
219                 v.addAll(events);
220             }
221         }
222     } finally {
223       partialEvent = null;
224       try {
225         if (reader != null) {
226           reader.close();
227         }
228       } catch (Exception e) {
229         e.printStackTrace();
230       }
231     }
232     return v;
233   }
234 
235     /**
236      * Decodes a String representing a number of events into a
237      * Vector of LoggingEvents.
238      * @param document to decode events from
239      * @return Vector of LoggingEvents
240      */
241   public Vector decodeEvents(final String document) {
242     if (document != null) {
243       if (document.trim().equals("")) {
244         return null;
245       }
246         String newDoc = null;
247         String newPartialEvent = null;
248         //separate the string into the last portion ending with
249         // </log4j:event> (which will be processed) and the
250         // partial event which will be combined and
251         // processed in the next section
252 
253         //if the document does not contain a record end,
254         // append it to the partial event string
255         if (document.lastIndexOf(RECORD_END) == -1) {
256             partialEvent = partialEvent + document;
257             return null;
258         }
259 
260         if (document.lastIndexOf(RECORD_END)
261                 + RECORD_END.length() < document.length()) {
262             newDoc = document.substring(0,
263                     document.lastIndexOf(RECORD_END) + RECORD_END.length());
264             newPartialEvent = document.substring(
265                     document.lastIndexOf(RECORD_END) + RECORD_END.length());
266         } else {
267             newDoc = document;
268         }
269         if (partialEvent != null) {
270             newDoc = partialEvent + newDoc;
271         }
272         partialEvent = newPartialEvent;
273         Document doc = parse(newDoc);
274         if (doc == null) {
275             return null;
276         }
277         return decodeEvents(doc);
278     }
279     return null;
280   }
281 
282   /**
283    * Converts the string data into an XML Document, and then soaks out the
284    * relevant bits to form a new LoggingEvent instance which can be used
285    * by any Log4j element locally.
286    * @param data XML fragment
287    * @return a single LoggingEvent or null
288    */
289   public LoggingEvent decode(final String data) {
290     Document document = parse(data);
291 
292     if (document == null) {
293       return null;
294     }
295 
296     Vector events = decodeEvents(document);
297 
298     if (events.size() > 0) {
299       return (LoggingEvent) events.firstElement();
300     }
301 
302     return null;
303   }
304 
305   /**
306    * Given a Document, converts the XML into a Vector of LoggingEvents.
307    * @param document XML document
308    * @return Vector of LoggingEvents
309    */
310   private Vector decodeEvents(final Document document) {
311     Vector events = new Vector();
312 
313     Logger logger;
314     long timeStamp;
315     Level level;
316     String threadName;
317     Object message = null;
318     String ndc = null;
319     String[] exception = null;
320     String className = null;
321     String methodName = null;
322     String fileName = null;
323     String lineNumber = null;
324     Hashtable properties = null;
325 
326     NodeList nl = document.getElementsByTagName("log4j:eventSet");
327     Node eventSet = nl.item(0);
328 
329     NodeList eventList = eventSet.getChildNodes();
330 
331     for (int eventIndex = 0; eventIndex < eventList.getLength();
332         eventIndex++) {
333         Node eventNode = eventList.item(eventIndex);
334       //ignore carriage returns in xml
335         if (eventNode.getNodeType() != Node.ELEMENT_NODE) {
336             continue;
337         }
338       logger = Logger.getLogger(eventNode.getAttributes().getNamedItem("logger").getNodeValue());
339       timeStamp = Long.parseLong(eventNode.getAttributes().getNamedItem("timestamp").getNodeValue());
340       level = Level.toLevel(eventNode.getAttributes().getNamedItem("level").getNodeValue());
341       threadName = eventNode.getAttributes().getNamedItem("thread").getNodeValue();
342 
343       NodeList list = eventNode.getChildNodes();
344       int listLength = list.getLength();
345 
346       if (listLength == 0) {
347         continue;
348       }
349 
350       for (int y = 0; y < listLength; y++) {
351         String tagName = list.item(y).getNodeName();
352 
353         if (tagName.equalsIgnoreCase("log4j:message")) {
354           message = getCData(list.item(y));
355         }
356 
357         if (tagName.equalsIgnoreCase("log4j:NDC")) {
358           ndc = getCData(list.item(y));
359         }
360         //still support receiving of MDC and convert to properties
361         if (tagName.equalsIgnoreCase("log4j:MDC")) {
362           properties = new Hashtable();
363           NodeList propertyList = list.item(y).getChildNodes();
364           int propertyLength = propertyList.getLength();
365 
366           for (int i = 0; i < propertyLength; i++) {
367             String propertyTag = propertyList.item(i).getNodeName();
368 
369             if (propertyTag.equalsIgnoreCase("log4j:data")) {
370               Node property = propertyList.item(i);
371               String name =
372                 property.getAttributes().getNamedItem("name").getNodeValue();
373               String value =
374                 property.getAttributes().getNamedItem("value").getNodeValue();
375               properties.put(name, value);
376             }
377           }
378         }
379 
380         if (tagName.equalsIgnoreCase("log4j:throwable")) {
381             String exceptionString = getCData(list.item(y));
382             if (exceptionString != null && !exceptionString.trim().equals("")) {
383                 exception = new String[] {exceptionString.trim()
384             };
385           }
386         }
387 
388         if (tagName.equalsIgnoreCase("log4j:locationinfo")) {
389           className =
390             list.item(y).getAttributes().getNamedItem("class").getNodeValue();
391           methodName =
392             list.item(y).getAttributes().getNamedItem("method").getNodeValue();
393           fileName =
394             list.item(y).getAttributes().getNamedItem("file").getNodeValue();
395           lineNumber =
396             list.item(y).getAttributes().getNamedItem("line").getNodeValue();
397         }
398 
399         if (tagName.equalsIgnoreCase("log4j:properties")) {
400           if (properties == null) {
401               properties = new Hashtable();
402           }
403           NodeList propertyList = list.item(y).getChildNodes();
404           int propertyLength = propertyList.getLength();
405 
406           for (int i = 0; i < propertyLength; i++) {
407             String propertyTag = propertyList.item(i).getNodeName();
408 
409             if (propertyTag.equalsIgnoreCase("log4j:data")) {
410               Node property = propertyList.item(i);
411               String name =
412                 property.getAttributes().getNamedItem("name").getNodeValue();
413               String value =
414                 property.getAttributes().getNamedItem("value").getNodeValue();
415               properties.put(name, value);
416             }
417           }
418         }
419 
420           /**
421            * We add all the additional properties to the properties
422            * hashtable. Override properties that already exist
423            */
424           if (additionalProperties.size() > 0) {
425               if (properties == null) {
426                   properties = new Hashtable(additionalProperties);
427               }
428               Iterator i = additionalProperties.entrySet().iterator();
429               while (i.hasNext()) {
430                   Map.Entry e = (Map.Entry) i.next();
431                   properties.put(e.getKey(), e.getValue());
432               }
433           }
434       }
435 
436       LocationInfo info;
437       if ((fileName != null)
438               || (className != null)
439               || (methodName != null)
440               || (lineNumber != null)) {
441           info = new LocationInfo(fileName, className, methodName, lineNumber);
442       } else {
443         info = LocationInfo.NA_LOCATION_INFO;
444       }
445       ThrowableInformation throwableInfo = null;
446       if (exception != null) {
447           throwableInfo = new ThrowableInformation(exception);
448       }
449 
450         LoggingEvent loggingEvent = new LoggingEvent(null,
451                 logger, timeStamp, level, message,
452                 threadName,
453                 throwableInfo,
454                 ndc,
455                 info,
456                 properties);
457 
458 
459       events.add(loggingEvent);
460 
461       message = null;
462       ndc = null;
463       exception = null;
464       className = null;
465       methodName = null;
466       fileName = null;
467       lineNumber = null;
468       properties = null;
469     }
470 
471     return events;
472   }
473 
474     /**
475      * Get contents of CDATASection.
476      * @param n CDATASection
477      * @return text content of all text or CDATA children of node.
478      */
479   private String getCData(final Node n) {
480     StringBuffer buf = new StringBuffer();
481     NodeList nl = n.getChildNodes();
482 
483     for (int x = 0; x < nl.getLength(); x++) {
484       Node innerNode = nl.item(x);
485 
486       if (
487         (innerNode.getNodeType() == Node.TEXT_NODE)
488           || (innerNode.getNodeType() == Node.CDATA_SECTION_NODE)) {
489         buf.append(innerNode.getNodeValue());
490       }
491     }
492 
493     return buf.toString();
494   }
495 }