/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j.xml;

import org.apache.log4j.Layout;
import org.apache.log4j.LayoutTest;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.NDC;
import org.apache.log4j.MDC;
import org.apache.log4j.spi.LoggingEvent;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.xml.sax.InputSource;

import java.io.Reader;
import java.io.StringReader;
import java.util.Hashtable;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/**
 * Test for XMLLayout.
 *
 * @author Curt Arnold
 */
public class XMLLayoutTest extends LayoutTest {
    /**
     * Construct new instance of XMLLayoutTest.
     *
     * @param testName test name.
     */
    public XMLLayoutTest(final String testName) {
	super(testName, "text/plain", false, null, null);
    }

    /**
     * Clear MDC and NDC before test.
     */
    public void setUp() {
	NDC.clear();
	if (MDC.getContext() != null) {
	    MDC.getContext().clear();
	}
    }

    /**
     * Clear MDC and NDC after test.
     */
    public void tearDown() {
	setUp();
    }

    /**
     * @{inheritDoc}
     */
    protected Layout createLayout() {
	return new XMLLayout();
    }

    /**
     * Parses the string as the body of an XML document and returns the document
     * element.
     * 
     * @param source source string.
     * @return document element.
     * @throws Exception if parser can not be constructed or source is not a valid
     *                   XML document.
     */
    private Element parse(final String source) throws Exception {
	DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	factory.setNamespaceAware(false);
	factory.setCoalescing(true);

	DocumentBuilder builder = factory.newDocumentBuilder();
	Reader reader = new StringReader(source);
	Document doc = builder.parse(new InputSource(reader));

	return doc.getDocumentElement();
    }

    /**
     * Checks a log4j:event element against expectations.
     * 
     * @param element element, may not be null.
     * @param event   event, may not be null.
     */
    private void checkEventElement(final Element element, final LoggingEvent event) {
	assertEquals("log4j:event", element.getTagName());
	assertEquals(event.getLoggerName(), element.getAttribute("logger"));
	assertEquals(Long.toString(event.timeStamp), element.getAttribute("timestamp"));
	assertEquals(event.getLevel().toString(), element.getAttribute("level"));
	assertEquals(event.getThreadName(), element.getAttribute("thread"));
    }

    /**
     * Checks a log4j:message element against expectations.
     * 
     * @param element element, may not be null.
     * @param message expected message.
     */
    private void checkMessageElement(final Element element, final String message) {
	assertEquals("log4j:message", element.getTagName());

	Node messageNode = element.getFirstChild();
	assertNotNull(messageNode);
	assertEquals(Node.TEXT_NODE, messageNode.getNodeType());
	assertEquals(message, messageNode.getNodeValue());
	assertNull(messageNode.getNextSibling());
    }

    /**
     * Checks a log4j:message element against expectations.
     * 
     * @param element element, may not be null.
     * @param message expected message.
     */
    private void checkNDCElement(final Element element, final String message) {
	assertEquals("log4j:NDC", element.getTagName());

	Node messageNode = element.getFirstChild();
	assertNotNull(messageNode);
	assertEquals(Node.TEXT_NODE, messageNode.getNodeType());
	assertEquals(message, messageNode.getNodeValue());
	assertNull(messageNode.getNextSibling());
    }

    /**
     * Checks a log4j:throwable element against expectations.
     * 
     * @param element element, may not be null.
     * @param ex      exception, may not be null.
     */
    private void checkThrowableElement(final Element element, final Exception ex) {
	assertEquals("log4j:throwable", element.getTagName());

	Node messageNode = element.getFirstChild();
	assertNotNull(messageNode);
	assertEquals(Node.TEXT_NODE, messageNode.getNodeType());

	String msg = ex.toString();
	assertEquals(msg, messageNode.getNodeValue().substring(0, msg.length()));
	assertNull(messageNode.getNextSibling());
    }

    /**
     * Checks a log4j:properties element against expectations.
     * 
     * @param element element, may not be null.
     * @param key     key.
     * @param value   value.
     */
    private void checkPropertiesElement(final Element element, final String key, final String value) {
	assertEquals("log4j:properties", element.getTagName());

	int childNodeCount = 0;
	for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) {
	    if (child.getNodeType() == Node.ELEMENT_NODE) {
		assertEquals("log4j:data", child.getNodeName());
		Element childElement = (Element) child;
		assertEquals(key, childElement.getAttribute("name"));
		assertEquals(value, childElement.getAttribute("value"));
		childNodeCount++;
	    }
	}
	assertEquals(1, childNodeCount);
    }

    /**
     * Tests formatted results.
     * 
     * @throws Exception if parser can not be constructed or source is not a valid
     *                   XML document.
     */
    public void testFormat() throws Exception {
	Logger logger = Logger.getLogger("org.apache.log4j.xml.XMLLayoutTest");
	LoggingEvent event = new LoggingEvent("org.apache.log4j.Logger", logger, Level.INFO, "Hello, World", null);
	XMLLayout layout = (XMLLayout) createLayout();
	String result = layout.format(event);
	Element parsedResult = parse(result);
	checkEventElement(parsedResult, event);

	int childElementCount = 0;

	for (Node node = parsedResult.getFirstChild(); node != null; node = node.getNextSibling()) {
	    switch (node.getNodeType()) {
	    case Node.ELEMENT_NODE:
		childElementCount++;
		checkMessageElement((Element) node, "Hello, World");

		break;

	    case Node.COMMENT_NODE:
		break;

	    case Node.TEXT_NODE:

		// should only be whitespace
		break;

	    default:
		fail("Unexpected node type");

		break;
	    }
	}

	assertEquals(1, childElementCount);
    }

    /**
     * Tests formatted results with an exception.
     * 
     * @throws Exception if parser can not be constructed or source is not a valid
     *                   XML document.
     */
    public void testFormatWithException() throws Exception {
	Logger logger = Logger.getLogger("org.apache.log4j.xml.XMLLayoutTest");
	Exception ex = new IllegalArgumentException("'foo' is not a valid name");
	LoggingEvent event = new LoggingEvent("org.apache.log4j.Logger", logger, Level.INFO, "Hello, World", ex);
	XMLLayout layout = (XMLLayout) createLayout();
	String result = layout.format(event);
	Element parsedResult = parse(result);
	checkEventElement(parsedResult, event);

	int childElementCount = 0;

	for (Node node = parsedResult.getFirstChild(); node != null; node = node.getNextSibling()) {
	    switch (node.getNodeType()) {
	    case Node.ELEMENT_NODE:
		childElementCount++;

		if (childElementCount == 1) {
		    checkMessageElement((Element) node, "Hello, World");
		} else {
		    checkThrowableElement((Element) node, ex);
		}

		break;

	    case Node.COMMENT_NODE:
		break;

	    case Node.TEXT_NODE:

		// should only be whitespace
		break;

	    default:
		fail("Unexpected node type");

		break;
	    }
	}

	assertEquals(2, childElementCount);
    }

    /**
     * Tests formatted results with an exception.
     * 
     * @throws Exception if parser can not be constructed or source is not a valid
     *                   XML document.
     */
    public void testFormatWithNDC() throws Exception {
	Logger logger = Logger.getLogger("org.apache.log4j.xml.XMLLayoutTest");
	NDC.push("NDC goes here");

	LoggingEvent event = new LoggingEvent("org.apache.log4j.Logger", logger, Level.INFO, "Hello, World", null);
	XMLLayout layout = (XMLLayout) createLayout();
	String result = layout.format(event);
	NDC.pop();

	Element parsedResult = parse(result);
	checkEventElement(parsedResult, event);

	int childElementCount = 0;

	for (Node node = parsedResult.getFirstChild(); node != null; node = node.getNextSibling()) {
	    switch (node.getNodeType()) {
	    case Node.ELEMENT_NODE:
		childElementCount++;

		if (childElementCount == 1) {
		    checkMessageElement((Element) node, "Hello, World");
		} else {
		    checkNDCElement((Element) node, "NDC goes here");
		}

		break;

	    case Node.COMMENT_NODE:
		break;

	    case Node.TEXT_NODE:

		// should only be whitespace
		break;

	    default:
		fail("Unexpected node type");

		break;
	    }
	}

	assertEquals(2, childElementCount);
    }

    /**
     * Tests getLocationInfo and setLocationInfo.
     */
    public void testGetSetLocationInfo() {
	XMLLayout layout = new XMLLayout();
	assertEquals(false, layout.getLocationInfo());
	layout.setLocationInfo(true);
	assertEquals(true, layout.getLocationInfo());
	layout.setLocationInfo(false);
	assertEquals(false, layout.getLocationInfo());
    }

    /**
     * Tests activateOptions().
     */
    public void testActivateOptions() {
	XMLLayout layout = new XMLLayout();
	layout.activateOptions();
    }

    /**
     * Level with arbitrary toString value.
     */
    private static final class ProblemLevel extends Level {
	private static final long serialVersionUID = 1L;

	/**
	 * Construct new instance.
	 * 
	 * @param levelName level name, may not be null.
	 */
	public ProblemLevel(final String levelName) {
	    super(6000, levelName, 6);
	}
    }

    /**
     * Tests problematic characters in multiple fields.
     * 
     * @throws Exception if parser can not be constructed or source is not a valid
     *                   XML document.
     */
    public void testProblemCharacters() throws Exception {
	String problemName = "com.example.bar<>&\"'";
	Logger logger = Logger.getLogger(problemName);
	Level level = new ProblemLevel(problemName);
	Exception ex = new IllegalArgumentException(problemName);
	String threadName = Thread.currentThread().getName();
	Thread.currentThread().setName(problemName);
	NDC.push(problemName);
	Hashtable mdcMap = MDC.getContext();
	if (mdcMap != null) {
	    mdcMap.clear();
	}
	MDC.put(problemName, problemName);
	LoggingEvent event = new LoggingEvent(problemName, logger, level, problemName, ex);
	XMLLayout layout = (XMLLayout) createLayout();
	layout.setProperties(true);
	String result = layout.format(event);
	mdcMap = MDC.getContext();
	if (mdcMap != null) {
	    mdcMap.clear();
	}
	Thread.currentThread().setName(threadName);

	Element parsedResult = parse(result);
	checkEventElement(parsedResult, event);

	int childElementCount = 0;

	for (Node node = parsedResult.getFirstChild(); node != null; node = node.getNextSibling()) {
	    switch (node.getNodeType()) {
	    case Node.ELEMENT_NODE:
		childElementCount++;
		switch (childElementCount) {
		case 1:
		    checkMessageElement((Element) node, problemName);
		    break;

		case 2:
		    checkNDCElement((Element) node, problemName);
		    break;

		case 3:
		    checkThrowableElement((Element) node, ex);
		    break;

		case 4:
		    checkPropertiesElement((Element) node, problemName, problemName);
		    break;

		default:
		    fail("Unexpected element");
		    break;
		}

		break;

	    case Node.COMMENT_NODE:
		break;

	    case Node.TEXT_NODE:

		// should only be whitespace
		break;

	    default:
		fail("Unexpected node type");

		break;
	    }
	}
    }

    /**
     * Tests CDATA element within NDC content. See bug 37560.
     */
    public void testNDCWithCDATA() throws Exception {
	Logger logger = Logger.getLogger("com.example.bar");
	Level level = Level.INFO;
	String ndcMessage = "<envelope><faultstring><![CDATA[The EffectiveDate]]></faultstring><envelope>";
	NDC.push(ndcMessage);
	LoggingEvent event = new LoggingEvent("com.example.bar", logger, level, "Hello, World", null);
	Layout layout = createLayout();
	String result = layout.format(event);
	NDC.clear();
	Element parsedResult = parse(result);
	NodeList ndcs = parsedResult.getElementsByTagName("log4j:NDC");
	assertEquals(1, ndcs.getLength());
	StringBuffer buf = new StringBuffer();
	for (Node child = ndcs.item(0).getFirstChild(); child != null; child = child.getNextSibling()) {
	    buf.append(child.getNodeValue());
	}
	assertEquals(ndcMessage, buf.toString());
    }

    /**
     * Tests CDATA element within exception. See bug 37560.
     */
    public void testExceptionWithCDATA() throws Exception {
	Logger logger = Logger.getLogger("com.example.bar");
	Level level = Level.INFO;
	String exceptionMessage = "<envelope><faultstring><![CDATA[The EffectiveDate]]></faultstring><envelope>";
	LoggingEvent event = new LoggingEvent("com.example.bar", logger, level, "Hello, World",
		new Exception(exceptionMessage));
	Layout layout = createLayout();
	String result = layout.format(event);
	Element parsedResult = parse(result);
	NodeList throwables = parsedResult.getElementsByTagName("log4j:throwable");
	assertEquals(1, throwables.getLength());
	StringBuffer buf = new StringBuffer();
	for (Node child = throwables.item(0).getFirstChild(); child != null; child = child.getNextSibling()) {
	    buf.append(child.getNodeValue());
	}
	assertTrue(buf.toString().indexOf(exceptionMessage) != -1);
    }

}
