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.Layout;
21  import org.apache.log4j.LayoutTest;
22  import org.apache.log4j.Level;
23  import org.apache.log4j.Logger;
24  import org.apache.log4j.NDC;
25  import org.apache.log4j.MDC;
26  import org.apache.log4j.spi.LoggingEvent;
27  
28  import org.w3c.dom.Document;
29  import org.w3c.dom.Element;
30  import org.w3c.dom.Node;
31  import org.w3c.dom.NodeList;
32  
33  import org.xml.sax.InputSource;
34  
35  import java.io.Reader;
36  import java.io.StringReader;
37  import java.util.Hashtable;
38  
39  import javax.xml.parsers.DocumentBuilder;
40  import javax.xml.parsers.DocumentBuilderFactory;
41  
42  
43  /**
44   * Test for XMLLayout.
45   *
46   * @author Curt Arnold
47   */
48  public class XMLLayoutTest extends LayoutTest {
49    /**
50     * Construct new instance of XMLLayoutTest.
51     *
52     * @param testName test name.
53     */
54    public XMLLayoutTest(final String testName) {
55      super(testName, "text/plain", false, null, null);
56    }
57  
58      /**
59       * Clear MDC and NDC before test.
60       */
61    public void setUp() {
62        NDC.clear();
63        if (MDC.getContext() != null) {
64          MDC.getContext().clear();
65        }
66    }
67  
68      /**
69       * Clear MDC and NDC after test.
70       */
71    public void tearDown() {
72        setUp();
73    }
74  
75    /**
76     * @{inheritDoc}
77     */
78    protected Layout createLayout() {
79      return new XMLLayout();
80    }
81  
82    /**
83     * Parses the string as the body of an XML document and returns the document element.
84     * @param source source string.
85     * @return document element.
86     * @throws Exception if parser can not be constructed or source is not a valid XML document.
87     */
88    private Element parse(final String source) throws Exception {
89      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
90      factory.setNamespaceAware(false);
91      factory.setCoalescing(true);
92  
93      DocumentBuilder builder = factory.newDocumentBuilder();
94      Reader reader = new StringReader(source);
95      Document doc = builder.parse(new InputSource(reader));
96  
97      return doc.getDocumentElement();
98    }
99  
100   /**
101    * Checks a log4j:event element against expectations.
102    * @param element element, may not be null.
103    * @param event event, may not be null.
104    */
105   private void checkEventElement(
106     final Element element, final LoggingEvent event) {
107     assertEquals("log4j:event", element.getTagName());
108     assertEquals(
109       event.getLoggerName(), element.getAttribute("logger"));
110     assertEquals(
111       Long.toString(event.timeStamp), element.getAttribute("timestamp"));
112     assertEquals(event.getLevel().toString(), element.getAttribute("level"));
113     assertEquals(event.getThreadName(), element.getAttribute("thread"));
114   }
115 
116   /**
117    * Checks a log4j:message element against expectations.
118    * @param element element, may not be null.
119    * @param message expected message.
120    */
121   private void checkMessageElement(
122     final Element element, final String message) {
123     assertEquals("log4j:message", element.getTagName());
124 
125     Node messageNode = element.getFirstChild();
126     assertNotNull(messageNode);
127     assertEquals(Node.TEXT_NODE, messageNode.getNodeType());
128     assertEquals(message, messageNode.getNodeValue());
129     assertNull(messageNode.getNextSibling());
130   }
131 
132   /**
133    * Checks a log4j:message element against expectations.
134    * @param element element, may not be null.
135    * @param message expected message.
136    */
137   private void checkNDCElement(final Element element, final String message) {
138     assertEquals("log4j:NDC", element.getTagName());
139 
140     Node messageNode = element.getFirstChild();
141     assertNotNull(messageNode);
142     assertEquals(Node.TEXT_NODE, messageNode.getNodeType());
143     assertEquals(message, messageNode.getNodeValue());
144     assertNull(messageNode.getNextSibling());
145   }
146 
147   /**
148    * Checks a log4j:throwable element against expectations.
149    * @param element element, may not be null.
150    * @param ex exception, may not be null.
151    */
152   private void checkThrowableElement(
153     final Element element, final Exception ex) {
154     assertEquals("log4j:throwable", element.getTagName());
155 
156     Node messageNode = element.getFirstChild();
157     assertNotNull(messageNode);
158     assertEquals(Node.TEXT_NODE, messageNode.getNodeType());
159 
160     String msg = ex.toString();
161     assertEquals(msg, messageNode.getNodeValue().substring(0, msg.length()));
162     assertNull(messageNode.getNextSibling());
163   }
164 
165     /**
166      * Checks a log4j:properties element against expectations.
167      * @param element element, may not be null.
168      * @param key key.
169      * @param value value.
170      */
171     private void checkPropertiesElement(
172       final Element element, final String key, final String value) {
173       assertEquals("log4j:properties", element.getTagName());
174 
175       int childNodeCount = 0;
176       for(Node child = element.getFirstChild();
177                child != null;
178                child = child.getNextSibling()) {
179           if (child.getNodeType() == Node.ELEMENT_NODE) {
180               assertEquals("log4j:data", child.getNodeName());
181               Element childElement = (Element) child;
182               assertEquals(key, childElement.getAttribute("name"));
183               assertEquals(value, childElement.getAttribute("value"));
184               childNodeCount++;
185           }
186       }
187       assertEquals(1, childNodeCount);  
188     }
189 
190   /**
191    * Tests formatted results.
192    * @throws Exception if parser can not be constructed or source is not a valid XML document.
193    */
194   public void testFormat() throws Exception {
195     Logger logger = Logger.getLogger("org.apache.log4j.xml.XMLLayoutTest");
196     LoggingEvent event =
197       new LoggingEvent(
198         "org.apache.log4j.Logger", logger, Level.INFO, "Hello, World", null);
199     XMLLayout layout = (XMLLayout) createLayout();
200     String result = layout.format(event);
201     Element parsedResult = parse(result);
202     checkEventElement(parsedResult, event);
203 
204     int childElementCount = 0;
205 
206     for (
207       Node node = parsedResult.getFirstChild(); node != null;
208         node = node.getNextSibling()) {
209       switch (node.getNodeType()) {
210       case Node.ELEMENT_NODE:
211         childElementCount++;
212         checkMessageElement((Element) node, "Hello, World");
213 
214         break;
215 
216       case Node.COMMENT_NODE:
217         break;
218 
219       case Node.TEXT_NODE:
220 
221         //  should only be whitespace
222         break;
223 
224       default:
225         fail("Unexpected node type");
226 
227         break;
228       }
229     }
230 
231     assertEquals(1, childElementCount);
232   }
233 
234   /**
235    * Tests formatted results with an exception.
236    * @throws Exception if parser can not be constructed or source is not a valid XML document.
237    */
238   public void testFormatWithException() throws Exception {
239     Logger logger = Logger.getLogger("org.apache.log4j.xml.XMLLayoutTest");
240     Exception ex = new IllegalArgumentException("'foo' is not a valid name");
241     LoggingEvent event =
242       new LoggingEvent(
243         "org.apache.log4j.Logger", logger, Level.INFO, "Hello, World", ex);
244     XMLLayout layout = (XMLLayout) createLayout();
245     String result = layout.format(event);
246     Element parsedResult = parse(result);
247     checkEventElement(parsedResult, event);
248 
249     int childElementCount = 0;
250 
251     for (
252       Node node = parsedResult.getFirstChild(); node != null;
253         node = node.getNextSibling()) {
254       switch (node.getNodeType()) {
255       case Node.ELEMENT_NODE:
256         childElementCount++;
257 
258         if (childElementCount == 1) {
259           checkMessageElement((Element) node, "Hello, World");
260         } else {
261           checkThrowableElement((Element) node, ex);
262         }
263 
264         break;
265 
266       case Node.COMMENT_NODE:
267         break;
268 
269       case Node.TEXT_NODE:
270 
271         //  should only be whitespace
272         break;
273 
274       default:
275         fail("Unexpected node type");
276 
277         break;
278       }
279     }
280 
281     assertEquals(2, childElementCount);
282   }
283 
284   /**
285    * Tests formatted results with an exception.
286    * @throws Exception if parser can not be constructed or source is not a valid XML document.
287    */
288   public void testFormatWithNDC() throws Exception {
289     Logger logger = Logger.getLogger("org.apache.log4j.xml.XMLLayoutTest");
290     NDC.push("NDC goes here");
291 
292     LoggingEvent event =
293       new LoggingEvent(
294         "org.apache.log4j.Logger", logger, Level.INFO, "Hello, World", null);
295     XMLLayout layout = (XMLLayout) createLayout();
296     String result = layout.format(event);
297     NDC.pop();
298 
299     Element parsedResult = parse(result);
300     checkEventElement(parsedResult, event);
301 
302     int childElementCount = 0;
303 
304     for (
305       Node node = parsedResult.getFirstChild(); node != null;
306         node = node.getNextSibling()) {
307       switch (node.getNodeType()) {
308       case Node.ELEMENT_NODE:
309         childElementCount++;
310 
311         if (childElementCount == 1) {
312           checkMessageElement((Element) node, "Hello, World");
313         } else {
314           checkNDCElement((Element) node, "NDC goes here");
315         }
316 
317         break;
318 
319       case Node.COMMENT_NODE:
320         break;
321 
322       case Node.TEXT_NODE:
323 
324         //  should only be whitespace
325         break;
326 
327       default:
328         fail("Unexpected node type");
329 
330         break;
331       }
332     }
333 
334     assertEquals(2, childElementCount);
335   }
336 
337   /**
338    * Tests getLocationInfo and setLocationInfo.
339    */
340   public void testGetSetLocationInfo() {
341     XMLLayout layout = new XMLLayout();
342     assertEquals(false, layout.getLocationInfo());
343     layout.setLocationInfo(true);
344     assertEquals(true, layout.getLocationInfo());
345     layout.setLocationInfo(false);
346     assertEquals(false, layout.getLocationInfo());
347   }
348 
349   /**
350    * Tests activateOptions().
351    */
352   public void testActivateOptions() {
353     XMLLayout layout = new XMLLayout();
354     layout.activateOptions();
355   }
356 
357     /**
358      * Level with arbitrary toString value.
359      */
360     private static final class ProblemLevel extends Level {
361         private static final long serialVersionUID = 1L;
362         /**
363          * Construct new instance.
364          * @param levelName level name, may not be null.
365          */
366         public ProblemLevel(final String levelName) {
367             super(6000, levelName, 6);
368         }
369     }
370 
371     /**
372      * Tests problematic characters in multiple fields.
373      * @throws Exception if parser can not be constructed or source is not a valid XML document.
374      */
375     public void testProblemCharacters() throws Exception {
376       String problemName = "com.example.bar<>&\"'";
377       Logger logger = Logger.getLogger(problemName);
378       Level level = new ProblemLevel(problemName);
379       Exception ex = new IllegalArgumentException(problemName);
380       String threadName = Thread.currentThread().getName();
381       Thread.currentThread().setName(problemName);
382       NDC.push(problemName);
383       Hashtable mdcMap = MDC.getContext();
384       if (mdcMap != null) {
385           mdcMap.clear();
386       }
387       MDC.put(problemName, problemName);
388       LoggingEvent event =
389         new LoggingEvent(
390           problemName, logger, level, problemName, ex);
391       XMLLayout layout = (XMLLayout) createLayout();
392       layout.setProperties(true);
393       String result = layout.format(event);
394       mdcMap = MDC.getContext();
395       if (mdcMap != null) {
396           mdcMap.clear();
397       }
398       Thread.currentThread().setName(threadName);
399 
400       Element parsedResult = parse(result);
401       checkEventElement(parsedResult, event);
402 
403       int childElementCount = 0;
404 
405       for (
406         Node node = parsedResult.getFirstChild(); node != null;
407           node = node.getNextSibling()) {
408         switch (node.getNodeType()) {
409         case Node.ELEMENT_NODE:
410           childElementCount++;
411           switch(childElementCount) {
412               case 1:
413               checkMessageElement((Element) node, problemName);
414               break;
415 
416               case 2:
417               checkNDCElement((Element) node, problemName);
418               break;
419 
420               case 3:
421               checkThrowableElement((Element) node, ex);
422               break;
423 
424               case 4:
425               checkPropertiesElement((Element) node, problemName, problemName);
426               break;
427 
428               default:
429               fail("Unexpected element");
430               break;
431           }
432 
433           break;
434 
435         case Node.COMMENT_NODE:
436           break;
437 
438         case Node.TEXT_NODE:
439 
440           //  should only be whitespace
441           break;
442 
443         default:
444           fail("Unexpected node type");
445 
446           break;
447         }
448       }
449     }
450 
451     /**
452       * Tests CDATA element within NDC content.  See bug 37560.
453       */
454     public void testNDCWithCDATA() throws Exception {
455         Logger logger = Logger.getLogger("com.example.bar");
456         Level level = Level.INFO;
457         String ndcMessage ="<envelope><faultstring><![CDATA[The EffectiveDate]]></faultstring><envelope>";
458         NDC.push(ndcMessage);
459         LoggingEvent event =
460           new LoggingEvent(
461             "com.example.bar", logger, level, "Hello, World", null);
462         Layout layout = createLayout();
463         String result = layout.format(event);
464         NDC.clear();
465         Element parsedResult = parse(result);
466         NodeList ndcs = parsedResult.getElementsByTagName("log4j:NDC");
467         assertEquals(1, ndcs.getLength());
468         StringBuffer buf = new StringBuffer();
469         for(Node child = ndcs.item(0).getFirstChild();
470                 child != null;
471                 child = child.getNextSibling()) {
472             buf.append(child.getNodeValue());
473         }
474         assertEquals(ndcMessage, buf.toString());
475    }
476 
477     /**
478       * Tests CDATA element within exception.  See bug 37560.
479       */
480     public void testExceptionWithCDATA() throws Exception {
481         Logger logger = Logger.getLogger("com.example.bar");
482         Level level = Level.INFO;
483         String exceptionMessage ="<envelope><faultstring><![CDATA[The EffectiveDate]]></faultstring><envelope>";
484         LoggingEvent event =
485           new LoggingEvent(
486             "com.example.bar", logger, level, "Hello, World", new Exception(exceptionMessage));
487         Layout layout = createLayout();
488         String result = layout.format(event);
489         Element parsedResult = parse(result);
490         NodeList throwables = parsedResult.getElementsByTagName("log4j:throwable");
491         assertEquals(1, throwables.getLength());
492         StringBuffer buf = new StringBuffer();
493         for(Node child = throwables.item(0).getFirstChild();
494                 child != null;
495                 child = child.getNextSibling()) {
496             buf.append(child.getNodeValue());
497         }
498         assertTrue(buf.toString().indexOf(exceptionMessage) != -1);
499    }
500 
501 }