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 }