HTMLLayoutTest.java

/*
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v2.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.html;

import java.io.ByteArrayInputStream;
import java.util.List;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.xml.sax.EntityResolver;

import ch.qos.logback.classic.ClassicTestConstants;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.spi.PubThrowableProxy;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.testUtil.StringListAppender;
import ch.qos.logback.core.util.StatusPrinter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class HTMLLayoutTest {

    LoggerContext lc;
    Logger root;
    HTMLLayout layout;

    @BeforeEach
    public void setUp() throws Exception {
        lc = new LoggerContext();
        lc.setName("default");

        layout = new HTMLLayout();
        layout.setThrowableRenderer(new DefaultThrowableRenderer());
        layout.setContext(lc);
        layout.setPattern("%level%thread%msg");
        layout.start();

        root = lc.getLogger(Logger.ROOT_LOGGER_NAME);

    }

    @AfterEach
    public void tearDown() throws Exception {
        lc = null;
        layout = null;
    }

    @Test
    public void testHeader() throws Exception {
        String header = layout.getFileHeader();
        // System.out.println(header);

        Document doc = parseOutput(header + "</body></html>");
        Element rootElement = doc.getRootElement();
        assertNotNull(rootElement.element("body"));

    }

    @Test
    public void testPresentationHeader() throws Exception {
        String header = layout.getFileHeader();
        String presentationHeader = layout.getPresentationHeader();
        header = header + presentationHeader;
        // System.out.println(header);

        Document doc = parseOutput(header + "</table></body></html>");
        Element rootElement = doc.getRootElement();
        Element bodyElement = rootElement.element("body");
        Element tableElement = bodyElement.element("table");
        Element trElement = tableElement.element("tr");
        List<Element> elementList = trElement.elements();
        assertEquals("Level", elementList.get(0).getText());
        assertEquals("Thread", elementList.get(1).getText());
        assertEquals("Message", elementList.get(2).getText());
    }

    @Test
    public void testAppendThrowable() throws Exception {
        StringBuilder buf = new StringBuilder();
        PubThrowableProxy tp = new PubThrowableProxy();
        tp.setClassName("test1");
        tp.setMessage("msg1");

        StackTraceElement ste1 = new StackTraceElement("c1", "m1", "f1", 1);
        StackTraceElement ste2 = new StackTraceElement("c2", "m2", "f2", 2);

        StackTraceElementProxy[] stepArray = { new StackTraceElementProxy(ste1), new StackTraceElementProxy(ste2) };
        tp.setStackTraceElementProxyArray(stepArray);
        DefaultThrowableRenderer renderer = (DefaultThrowableRenderer) layout.getThrowableRenderer();

        renderer.render(buf, tp);
        System.out.println(buf.toString());
        String[] result = buf.toString().split(CoreConstants.LINE_SEPARATOR);
        System.out.println(result[0]);
        assertEquals("test1: msg1", result[0]);
        assertEquals(DefaultThrowableRenderer.TRACE_PREFIX + "at c1.m1(f1:1)", result[1]);
    }

    @Test
    public void testDoLayout() throws Exception {
        ILoggingEvent le = createLoggingEvent();

        String result = layout.getFileHeader();
        result += layout.getPresentationHeader();
        result += layout.doLayout(le);
        result += layout.getPresentationFooter();
        result += layout.getFileFooter();

        Document doc = parseOutput(result);
        Element rootElement = doc.getRootElement();
        rootElement.toString();

        // the rest of this test is very dependent of the output generated
        // by HTMLLayout. Given that the XML parser already verifies
        // that the result conforms to xhtml-strict, we may want to
        // skip the assertions below. However, the assertions below are another
        // *independent* way to check the output format.

        // head, body
        assertEquals(2, rootElement.elements().size());
        Element bodyElement = (Element) rootElement.elements().get(1);
        Element tableElement = (Element) bodyElement.elements().get(3);
        assertEquals("table", tableElement.getName());
        Element trElement = (Element) tableElement.elements().get(1);
        {
            Element tdElement = (Element) trElement.elements().get(0);
            assertEquals("DEBUG", tdElement.getText());
        }
        {
            Element tdElement = (Element) trElement.elements().get(1);
            String regex = ClassicTestConstants.NAKED_MAIN_REGEX;
            System.out.println(tdElement.getText());
            assertTrue(tdElement.getText().matches(regex));
        }
        {
            Element tdElement = (Element) trElement.elements().get(2);
            assertEquals("test message", tdElement.getText());
        }
    }

    @Test
    public void layoutWithException() throws Exception {
        layout.setPattern("%level %thread %msg %ex");
        LoggingEvent le = createLoggingEvent();
        le.setThrowableProxy(new ThrowableProxy(new Exception("test Exception")));
        String result = layout.doLayout(le);

        String stringToParse = layout.getFileHeader();
        stringToParse = stringToParse + layout.getPresentationHeader();
        stringToParse += result;
        stringToParse += "</table></body></html>";

        // System.out.println(stringToParse);

        Document doc = parseOutput(stringToParse);
        Element rootElement = doc.getRootElement();
        Element bodyElement = rootElement.element("body");
        Element tableElement = bodyElement.element("table");
        List<Element> trElementList = tableElement.elements();
        Element exceptionRowElement = trElementList.get(2);
        Element exceptionElement = exceptionRowElement.element("td");

        assertEquals(3, tableElement.elements().size());
        assertTrue(exceptionElement.getText().contains("java.lang.Exception: test Exception"));
    }

    @Test
    @Disabled
    public void rawLimit() throws Exception {
        StringBuilder sb = new StringBuilder();
        String header = layout.getFileHeader();
        assertTrue(header.startsWith(
                "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"));
        sb.append(header);
        sb.append(layout.getPresentationHeader());
        for (int i = 0; i < CoreConstants.TABLE_ROW_LIMIT * 3; i++) {
            sb.append(layout.doLayout(
                    new LoggingEvent(this.getClass().getName(), root, Level.DEBUG, "test message" + i, null, null)));
        }
        sb.append(layout.getPresentationFooter());
        sb.append(layout.getFileFooter());
        // check that the output adheres to xhtml-strict.dtd
        parseOutput(sb.toString());
    }

    private LoggingEvent createLoggingEvent() {
        return new LoggingEvent(this.getClass().getName(), root, Level.DEBUG, "test message", null, null);
    }

    Document parseOutput(String output) throws Exception {
        EntityResolver resolver = new XHTMLEntityResolver();
        SAXReader reader = new SAXReader();
        reader.setValidation(true);
        reader.setEntityResolver(resolver);
        return reader.read(new ByteArrayInputStream(output.getBytes()));
    }

    void configure(String file) throws JoranException {
        JoranConfigurator jc = new JoranConfigurator();
        jc.setContext(lc);
        jc.doConfigure(file);
    }

    @Test
    public void testConversionRuleSupportInHtmlLayout() throws JoranException {
        configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/htmlLayout0.xml");

        root.getAppender("LIST");
        String msg = "Simon says";
        root.debug(msg);
        StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
        assertNotNull(sla);
        StatusPrinter.print(lc);
        assertEquals(1, sla.strList.size());
        assertFalse(sla.strList.get(0).contains("PARSER_ERROR"));
    }
}