/*
 * 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);
    }

}
