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