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