Add HTMLLayout

git-svn-id: https://svn.apache.org/repos/asf/logging/log4j/branches/BRANCH_2_0_EXPERIMENTAL/rgoers@1127414 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
index 846a8af..3167c17 100644
--- a/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
@@ -45,10 +45,16 @@
 
     private Object externalContext = null;
 
+    private static final long JVM_START_TIME = System.currentTimeMillis();
+
     public LoggerContext() {
         reconfigure();
     }
 
+    public static long getStartTime() {
+        return JVM_START_TIME;
+    }
+
     public void setExternalContext(Object context) {
         this.externalContext = context;
     }
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java
index 8d85e60..bd75ab1 100644
--- a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java
@@ -33,19 +33,29 @@
  * This appender is primarily used for testing. Use in a real environment is discouraged as the
  * List could eventually grow to cause an OutOfMemoryError.
  */
-@Plugin(name="List",type="Core",elementType="appender",printObject=true)
+@Plugin(name = "List", type = "Core", elementType = "appender", printObject = true)
 public class ListAppender extends AppenderBase {
 
     private List<LogEvent> events = new ArrayList<LogEvent>();
 
     private List<String> messages = new ArrayList<String>();
 
+    private final boolean newLine;
+
     public ListAppender(String name) {
         super(name, null, null);
+        newLine = false;
     }
 
-    public ListAppender(String name, Filters filters, Layout layout) {
+    public ListAppender(String name, Filters filters, Layout layout, boolean newline) {
         super(name, filters, layout);
+        this.newLine = newline;
+        if (layout != null) {
+            byte[] bytes = layout.getHeader();
+            if (bytes != null) {
+                write(bytes);
+            }
+        }
     }
 
     public synchronized void append(LogEvent event) {
@@ -53,7 +63,41 @@
         if (layout == null) {
             events.add(event);
         } else {
-            messages.add(new String(layout.format(event)));
+            write(layout.format(event));
+        }
+    }
+
+    private void write(byte[] bytes) {
+        String str = new String(bytes);
+        if (newLine) {
+            int index = 0;
+            while (index < str.length()) {
+                int end = str.indexOf("\n", index);
+                if (index == end) {
+                    if (!messages.get(messages.size() - 1).equals("")) {
+                        messages.add("");
+                    }
+                } else if (end >= 0) {
+                    messages.add(str.substring(index, end));
+                } else {
+                    messages.add(str.substring(index));
+                    break;
+                }
+                index = end + 1;
+            }
+        } else {
+            messages.add(str);
+        }
+    }
+
+    public void stop() {
+        super.stop();
+        Layout layout = getLayout();
+        if (layout != null) {
+            byte[] bytes = layout.getFooter();
+            if (bytes != null) {
+                write(bytes);
+            }
         }
     }
 
@@ -72,6 +116,7 @@
 
     @PluginFactory
     public static ListAppender createAppender(@PluginAttr("name") String name,
+                                              @PluginAttr("entryPerNewLine") String newLine,
                                               @PluginElement("layout") Layout layout,
                                               @PluginElement("filters") Filters filters) {
 
@@ -80,6 +125,8 @@
             return null;
         }
 
-        return new ListAppender(name, filters, layout);
+        boolean nl = (newLine == null) ? false : Boolean.parseBoolean(newLine);
+
+        return new ListAppender(name, filters, layout, nl);
     }
 }
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java
index 0819b28..f63815f 100644
--- a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java
@@ -48,8 +48,6 @@
 
     private int count;
 
-    private byte[] header = null;
-
     private byte[] footer = null;
 
     public StringBuilder buffer = new StringBuilder();
@@ -86,13 +84,17 @@
     }
 
     public synchronized void setHeader(byte[] header) {
-        if (header == null) {
-            this.header = header;
+        if (header != null) {
+            try {
+                this.os.write(header, 0, header.length);
+            } catch (IOException ioe) {
+                logger.error("Unable to write header", ioe);
+            }
         }
     }
 
     public synchronized void setFooter(byte[] footer) {
-        if (footer == null) {
+        if (footer != null) {
             this.footer = footer;
         }
     }
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java
new file mode 100644
index 0000000..2d44458
--- /dev/null
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java
@@ -0,0 +1,110 @@
+/*
+ * 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.logging.log4j.core.helpers;
+
+
+/**
+ * Utility class for transforming strings.
+ */
+public class Transform {
+
+    private static final String CDATA_START = "<![CDATA[";
+    private static final String CDATA_END = "]]>";
+    private static final String CDATA_PSEUDO_END = "]]&gt;";
+    private static final String CDATA_EMBEDED_END = CDATA_END + CDATA_PSEUDO_END + CDATA_START;
+    private static final int CDATA_END_LEN = CDATA_END.length();
+
+    /**
+     * This method takes a string which may contain HTML tags (ie,
+     * &lt;b&gt;, &lt;table&gt;, etc) and replaces any
+     * '<',  '>' , '&' or '"'
+     * characters with respective predefined entity references.
+     *
+     * @param input The text to be converted.
+     * @return The input string with the special characters replaced.
+     */
+    static public String escapeTags(final String input) {
+        //Check if the string is null, zero length or devoid of special characters
+        // if so, return what was sent in.
+
+        if (input == null
+            || input.length() == 0
+            || (input.indexOf('"') == -1 &&
+            input.indexOf('&') == -1 &&
+            input.indexOf('<') == -1 &&
+            input.indexOf('>') == -1)) {
+            return input;
+        }
+
+        //Use a StringBuffer in lieu of String concatenation -- it is
+        //much more efficient this way.
+
+        StringBuilder buf = new StringBuilder(input.length() + 6);
+        char ch = ' ';
+
+        int len = input.length();
+        for (int i = 0; i < len; i++) {
+            ch = input.charAt(i);
+            if (ch > '>') {
+                buf.append(ch);
+            } else if (ch == '<') {
+                buf.append("&lt;");
+            } else if (ch == '>') {
+                buf.append("&gt;");
+            } else if (ch == '&') {
+                buf.append("&amp;");
+            } else if (ch == '"') {
+                buf.append("&quot;");
+            } else {
+                buf.append(ch);
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Ensures that embeded CDEnd strings (]]>) are handled properly
+     * within message, NDC and throwable tag text.
+     *
+     * @param buf StringBuffer holding the XML data to this point.  The
+     *            initial CDStart (<![CDATA[) and final CDEnd (]]>) of the CDATA
+     *            section are the responsibility of the calling method.
+     * @param str The String that is inserted into an existing CDATA Section within buf.
+     */
+    static public void appendEscapingCDATA(final StringBuilder buf,
+                                           final String str) {
+        if (str != null) {
+            int end = str.indexOf(CDATA_END);
+            if (end < 0) {
+                buf.append(str);
+            } else {
+                int start = 0;
+                while (end > -1) {
+                    buf.append(str.substring(start, end));
+                    buf.append(CDATA_EMBEDED_END);
+                    start = end + CDATA_END_LEN;
+                    if (start < str.length()) {
+                        end = str.indexOf(CDATA_END, start);
+                    } else {
+                        return;
+                    }
+                }
+                buf.append(str.substring(start));
+            }
+        }
+    }
+}
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/layout/HTMLLayout.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/layout/HTMLLayout.java
new file mode 100644
index 0000000..8b8a283
--- /dev/null
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/layout/HTMLLayout.java
@@ -0,0 +1,260 @@
+/*
+ * 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.logging.log4j.core.layout;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAttr;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.core.helpers.Transform;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.LineNumberReader;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+
+/**
+ * This layout outputs events in a HTML table.
+ * <p/>
+ * Appenders using this layout should have their encoding set to UTF-8 or UTF-16, otherwise events containing
+ * non ASCII characters could result in corrupted log files.
+ */
+@Plugin(name="HTMLLayout",type="Core",elementType="layout",printObject=true)
+public class HTMLLayout extends LayoutBase {
+
+    protected final int BUF_SIZE = 256;
+    protected final int MAX_CAPACITY = 1024;
+
+    static String TRACE_PREFIX = "<br>&nbsp;&nbsp;&nbsp;&nbsp;";
+
+    // output buffer appended to when format() is invoked
+    private StringBuilder sbuf = new StringBuilder(BUF_SIZE);
+
+    // Print no location info by default
+    protected final boolean locationInfo;
+
+    private static final String DEFAULT_TITLE = "Log4J Log Messages";
+
+    private static final String DEFAULT_CONTENT_TYPE = "text/html";
+
+    protected final String title;
+
+    protected final String contentType;
+
+    protected final Charset charset;
+
+    public HTMLLayout(boolean locationInfo, String title, String contentType, Charset charset) {
+        this.locationInfo = locationInfo;
+        this.title = title;
+        this.contentType = contentType;
+        this.charset = charset;
+    }
+
+    public byte[] format(LogEvent event) {
+
+        if (sbuf.capacity() > MAX_CAPACITY) {
+            sbuf = new StringBuilder(BUF_SIZE);
+        } else {
+            sbuf.setLength(0);
+        }
+
+        sbuf.append(LINE_SEP).append("<tr>").append(LINE_SEP);
+
+        sbuf.append("<td>");
+        sbuf.append(event.getMillis() - LoggerContext.getStartTime());
+        sbuf.append("</td>").append(LINE_SEP);
+
+        String escapedThread = Transform.escapeTags(event.getThreadName());
+        sbuf.append("<td title=\"" + escapedThread + " thread\">");
+        sbuf.append(escapedThread);
+        sbuf.append("</td>").append(LINE_SEP);
+
+        sbuf.append("<td title=\"Level\">");
+        if (event.getLevel().equals(Level.DEBUG)) {
+            sbuf.append("<font color=\"#339933\">");
+            sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
+            sbuf.append("</font>");
+        } else if (event.getLevel().isAtLeastAsSpecificAs(Level.WARN)) {
+            sbuf.append("<font color=\"#993300\"><strong>");
+            sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
+            sbuf.append("</strong></font>");
+        } else {
+            sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
+        }
+        sbuf.append("</td>").append(LINE_SEP);
+
+        String escapedLogger = Transform.escapeTags(event.getLoggerName());
+        if (escapedLogger.length() == 0) {
+            escapedLogger = "root";
+        }
+        sbuf.append("<td title=\"").append(escapedLogger).append(" category\">");
+        sbuf.append(escapedLogger);
+        sbuf.append("</td>").append(LINE_SEP);
+
+        if (locationInfo) {
+            StackTraceElement element = event.getSource();
+            sbuf.append("<td>");
+            sbuf.append(Transform.escapeTags(element.getFileName()));
+            sbuf.append(':');
+            sbuf.append(element.getLineNumber());
+            sbuf.append("</td>").append(LINE_SEP);
+        }
+
+        sbuf.append("<td title=\"Message\">");
+        sbuf.append(Transform.escapeTags(event.getMessage().getFormattedMessage()));
+        sbuf.append("</td>").append(LINE_SEP);
+        sbuf.append("</tr>").append(LINE_SEP);
+
+        if (event.getContextStack().size() > 0) {
+            sbuf.append(
+                "<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" colspan=\"6\" title=\"Nested Diagnostic Context\">");
+            sbuf.append("NDC: " + Transform.escapeTags(event.getContextStack().toString()));
+            sbuf.append("</td></tr>").append(LINE_SEP);
+        }
+
+
+        if (event.getContextMap().size() > 0) {
+            sbuf.append(
+                "<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" colspan=\"6\" title=\"Mapped Diagnostic Context\">");
+            sbuf.append("MDC: " + Transform.escapeTags(event.getContextMap().toString()));
+            sbuf.append("</td></tr>").append(LINE_SEP);
+        }
+
+        Throwable throwable = event.getThrown();
+        if (throwable != null) {
+            sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : xx-small;\" colspan=\"6\">");
+            appendThrowableAsHTML(throwable, sbuf);
+            sbuf.append("</td></tr>").append(LINE_SEP);
+        }
+
+        return sbuf.toString().getBytes(charset);
+    }
+
+    void appendThrowableAsHTML(Throwable throwable, StringBuilder sbuf) {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        try {
+            throwable.printStackTrace(pw);
+        } catch(RuntimeException ex) {
+        }
+        pw.flush();
+        LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
+        ArrayList<String> lines = new ArrayList<String>();
+        try {
+          String line = reader.readLine();
+          while(line != null) {
+            lines.add(line);
+            line = reader.readLine();
+          }
+        } catch(IOException ex) {
+            if (ex instanceof InterruptedIOException) {
+                Thread.currentThread().interrupt();
+            }
+            lines.add(ex.toString());
+        }
+        boolean first = true;
+        for (String line : lines) {
+            if (!first) {
+                sbuf.append(TRACE_PREFIX);
+            } else {
+                first = false;
+            }
+            sbuf.append(Transform.escapeTags(line));
+            sbuf.append(LINE_SEP);
+        }
+    }
+
+    /**
+     * Returns appropriate HTML headers.
+     */
+    @Override
+    public byte[] getHeader() {
+        StringBuilder sbuf = new StringBuilder();
+        sbuf.append(
+            "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">");
+        sbuf.append(LINE_SEP);
+        sbuf.append("<html>").append(LINE_SEP);
+        sbuf.append("<head>").append(LINE_SEP);
+        sbuf.append("<title>").append(title).append("</title>").append(LINE_SEP);
+        sbuf.append("<style type=\"text/css\">").append(LINE_SEP);
+        sbuf.append("<!--").append(LINE_SEP);
+        sbuf.append("body, table {font-family: arial,sans-serif; font-size: x-small;}").append(LINE_SEP);
+        sbuf.append("th {background: #336699; color: #FFFFFF; text-align: left;}").append(LINE_SEP);
+        sbuf.append("-->").append(LINE_SEP);
+        sbuf.append("</style>").append(LINE_SEP);
+        sbuf.append("</head>").append(LINE_SEP);
+        sbuf.append("<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">").append(LINE_SEP);
+        sbuf.append("<hr size=\"1\" noshade>").append(LINE_SEP);
+        sbuf.append("Log session start time " + new java.util.Date() + "<br>").append(LINE_SEP);
+        sbuf.append("<br>").append(LINE_SEP);
+        sbuf.append(
+            "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">");
+        sbuf.append(LINE_SEP);
+        sbuf.append("<tr>").append(LINE_SEP);
+        sbuf.append("<th>Time</th>").append(LINE_SEP);
+        sbuf.append("<th>Thread</th>").append(LINE_SEP);
+        sbuf.append("<th>Level</th>").append(LINE_SEP);
+        sbuf.append("<th>Logger</th>").append(LINE_SEP);
+        if (locationInfo) {
+            sbuf.append("<th>File:Line</th>").append(LINE_SEP);
+        }
+        sbuf.append("<th>Message</th>").append(LINE_SEP);
+        sbuf.append("</tr>").append(LINE_SEP);
+        return sbuf.toString().getBytes(charset);
+    }
+
+    /**
+     * Returns the appropriate HTML footers.
+     */
+    @Override
+    public byte[] getFooter() {
+        StringBuilder sbuf = new StringBuilder();
+        sbuf.append("</table>").append(LINE_SEP);
+        sbuf.append("<br>").append(LINE_SEP);
+        sbuf.append("</body></html>");
+        return sbuf.toString().getBytes(charset);
+    }
+
+    @PluginFactory
+    public static HTMLLayout createLayout(@PluginAttr("locationInfo") String locationInfo,
+                                          @PluginAttr("title") String title,
+                                          @PluginAttr("contentType") String contentType,
+                                          @PluginAttr("charset") String charset) {
+        Charset c = Charset.isSupported("UTF-8") ? Charset.forName("UTF-8") : Charset.defaultCharset();
+        if (charset != null) {
+            if (Charset.isSupported(charset)) {
+                c = Charset.forName(charset);
+            } else {
+                logger.error("Charset " + charset + " is not supported for layout, using " + c.displayName());
+            }
+        }
+        boolean info = locationInfo == null ? false : Boolean.valueOf(locationInfo);
+        if (title == null) {
+            title = DEFAULT_TITLE;
+        }
+        if (contentType == null) {
+            contentType = DEFAULT_CONTENT_TYPE;
+        }
+        return new HTMLLayout(info, title, contentType, c);
+    }
+}
diff --git a/log4j2-core/src/test/java/org/apache/logging/log4j/core/layout/HTMLLayoutTest.java b/log4j2-core/src/test/java/org/apache/logging/log4j/core/layout/HTMLLayoutTest.java
new file mode 100644
index 0000000..3b5b95a
--- /dev/null
+++ b/log4j2-core/src/test/java/org/apache/logging/log4j/core/layout/HTMLLayoutTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.logging.log4j.core.layout;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.BasicConfigurationFactory;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.ConsoleAppender;
+import org.apache.logging.log4j.core.appender.FileAppender;
+import org.apache.logging.log4j.core.appender.FileManager;
+import org.apache.logging.log4j.core.appender.ListAppender;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.util.Compare;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.FileOutputStream;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ *
+ */
+public class HTMLLayoutTest {
+    LoggerContext ctx = (LoggerContext) LogManager.getContext();
+    Logger root = ctx.getLogger("");
+
+    static ConfigurationFactory cf = new BasicConfigurationFactory();
+
+    @BeforeClass
+    public static void setupClass() {
+        ConfigurationFactory.setConfigurationFactory(cf);
+        LoggerContext ctx = (LoggerContext) LogManager.getContext();
+        ctx.reconfigure();
+    }
+
+    @AfterClass
+    public static void cleanupClass() {
+        ConfigurationFactory.removeConfigurationFactory(cf);
+    }
+
+    private static final String body =
+        "<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : xx-small;\" colspan=\"6\">java.lang.NullPointerException: test";
+
+
+    /**
+     * Test case for MDC conversion pattern.
+     */
+    @Test
+    public void testLayout() throws Exception {
+
+        // set up appender
+        HTMLLayout layout = HTMLLayout.createLayout("true", null, null, null);
+        ListAppender appender = new ListAppender("List", null, layout, true);
+        appender.start();
+
+        // set appender on root and set level to debug
+        root.addAppender(appender);
+        root.setLevel(Level.DEBUG);
+
+        // output starting message
+        root.debug("starting mdc pattern test");
+
+        root.debug("empty mdc");
+
+        ThreadContext.put("key1", "value1");
+        ThreadContext.put("key2", "value2");
+
+        root.debug("filled mdc");
+
+        ThreadContext.remove("key1");
+        ThreadContext.remove("key2");
+
+        root.error("finished mdc pattern test", new NullPointerException("test"));
+
+        appender.stop();
+
+        List<String> list = appender.getMessages();
+
+        assertTrue("Incorrect number of lines. Expected 93, actual " + list.size(), list.size() == 93);
+        assertTrue("Incorrect header", list.get(3).equals("<title>Log4J Log Messages</title>"));
+        assertTrue("Incorrect footer", list.get(92).equals("</body></html>"));
+        assertTrue("Incorrect body", list.get(61).equals(body));
+    }
+}