
Author: seb Date: Tue Sep 12 09:35:04 2006 New Revision: 559 Added: logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/helpers/Transform.java logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/html/ logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/html/HTMLLayout.java logback/trunk/logback-classic/src/test/java/ch/qos/logback/classic/html/ logback/trunk/logback-classic/src/test/java/ch/qos/logback/classic/html/HTMLLayoutTest.java Modified: logback/trunk/logback-classic/pom.xml logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/net/SMTPAppender.java logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableHandlingConverter.java logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java logback/trunk/pom.xml Log: - added HTMLLayout, work in progress - defaultConverterMap in PatternLayout now is public, to allow sharing of the map with HTMLLayout - added a Dom4j dependency for Classic tests, in pom.xml - a few re-format of classes, replacing tabs by spaces Modified: logback/trunk/logback-classic/pom.xml ============================================================================== --- logback/trunk/logback-classic/pom.xml (original) +++ logback/trunk/logback-classic/pom.xml Tue Sep 12 09:35:04 2006 @@ -42,7 +42,13 @@ <artifactId>mail</artifactId> <scope>compile</scope> </dependency> - + + <dependency> + <groupId>dom4j</groupId> + <artifactId>dom4j</artifactId> + <scope>test</scope> + </dependency> + <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> Modified: logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java ============================================================================== --- logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java (original) +++ logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java Tue Sep 12 09:35:04 2006 @@ -38,7 +38,7 @@ // FIXME fix exception handling - static final Map<String, String> defaultConverterMap = new HashMap<String, String>(); + public static final Map<String, String> defaultConverterMap = new HashMap<String, String>(); static { Added: logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/helpers/Transform.java ============================================================================== --- (empty file) +++ logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/helpers/Transform.java Tue Sep 12 09:35:04 2006 @@ -0,0 +1,98 @@ +/** + * Logback: the reliable, generic, fast and flexible logging framework. + * + * Copyright (C) 1999-2006, QOS.ch + * + * This library is free software, you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation. + */ +package ch.qos.logback.classic.helpers; + +/** + * Utility class for transforming strings. + * + * @author Ceki Gülcü + * @author Michael A. McAngus + */ +public class Transform { + private static final String CDATA_START = "<![CDATA["; + private static final String CDATA_END = "]]>"; + private static final String CDATA_PSEUDO_END = "]]>"; + 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, <b>, + * <table>, etc) and replaces any '<' and '>' characters with + * respective predefined entity references. + * + * @param input + * The text to be converted. + */ + public static String escapeTags(final String input) { + // Check if the string is null or zero length -- if so, return + // what was sent in. + if ((input == null) || (input.length() == 0) + || (input.indexOf("<") == -1 && input.indexOf(">") == -1)) { + return input; + } + + StringBuffer buf = new StringBuffer(input); + for (int i = 0; i < buf.length(); i++) { + char ch = buf.charAt(i); + if (ch == '<') { + buf.replace(i, i + 1, "<"); + } else if (ch == '>') { + buf.replace(i, i + 1, ">"); + } + } + return buf.toString(); + } + + // public static void appendEscapingCDATA(StringBuffer buf, String str) { + // + // } + + /** + * Ensures that embeded CDEnd strings (]]>) are handled properly within + * message, NDC and throwable tag text. + * + * @param output + * Writer. The initial CDSutart (<![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. + */ + public static void appendEscapingCDATA(StringBuffer output, String str) { + if (str == null) { + return; + } + + int end = str.indexOf(CDATA_END); + + if (end < 0) { + output.append(str); + + return; + } + + int start = 0; + + while (end > -1) { + output.append(str.substring(start, end)); + output.append(CDATA_EMBEDED_END); + start = end + CDATA_END_LEN; + + if (start < str.length()) { + end = str.indexOf(CDATA_END, start); + } else { + return; + } + } + + output.append(str.substring(start)); + } +} Added: logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/html/HTMLLayout.java ============================================================================== --- (empty file) +++ logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/html/HTMLLayout.java Tue Sep 12 09:35:04 2006 @@ -0,0 +1,424 @@ +/** + * Logback: the reliable, generic, fast and flexible logging framework. + * + * Copyright (C) 1999-2006, QOS.ch + * + * This library is free software, you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation. + */ + +package ch.qos.logback.classic.html; + +import ch.qos.logback.classic.ClassicLayout; +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.helpers.Transform; +import ch.qos.logback.classic.pattern.NopThrowableInformationConverter; +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableInformationConverter; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.classic.spi.ThrowableInformation; +import ch.qos.logback.core.LayoutBase; +import ch.qos.logback.core.pattern.Converter; +import ch.qos.logback.core.pattern.DynamicConverter; +import ch.qos.logback.core.pattern.parser.Node; +import ch.qos.logback.core.pattern.parser.Parser; +import ch.qos.logback.core.pattern.parser.ScanException; + +/** + * + * HTMLLayout outputs events in an HTML table. The content of the table columns + * are specified using a conversion pattern. See + * {@link ch.qos.logback.classic.PatternLayout} for documentation on the + * available patterns. + * + * @author Ceki Gülcü + * @author Sébastien Pennec + */ +public class HTMLLayout extends LayoutBase implements ClassicLayout { + + /** + * Default pattern string for log output. Currently set to the string <b>"%m" + * </b> which just prints the application supplied message. + */ + static final String DEFAULT_CONVERSION_PATTERN = "%date%thread%level%logger%mdc%msg"; + + static final String TRACE_PREFIX = "<br /> "; + protected final int BUF_SIZE = 256; + protected final int MAX_CAPACITY = 1024; + + private String pattern; + + private Converter head; + + //private String timezone; + private String title = "Logback Log Messages"; + private boolean locationInfo; + + private boolean internalCSS = false; + private String url2ExternalCSS = "http://logging.apache.org/log4j/docs/css/eventTable-1.0.css"; + + // Does our PatternConverter chain handle throwable on its own? + private boolean chainHandlesThrowable; + + // counter keeping track of the rows output + private long counter = 0; + + /** + * Constructs a PatternLayout using the DEFAULT_LAYOUT_PATTERN. + * + * The default pattern just produces the application supplied message. + */ + public HTMLLayout() { + pattern = DEFAULT_CONVERSION_PATTERN; + } + + /** + * Set the <b>ConversionPattern </b> option. This is the string which controls + * formatting and consists of a mix of literal content and conversion + * specifiers. + */ + public void setPattern(String conversionPattern) { + pattern = conversionPattern; + } + + /** + * Returns the value of the <b>ConversionPattern </b> option. + */ + public String getPattern() { + return pattern; + } + + /** + * Parses the pattern and creates the Converter linked list. + */ + public void start() { + try { + Parser p = new Parser(pattern); + if (getContext() != null) { + p.setStatusManager(getContext().getStatusManager()); + } + Node t = p.parse(); + this.head = p.compile(t, PatternLayout.defaultConverterMap); + postCompileProcessing(head); + DynamicConverter.startConverters(this.head); + } catch (ScanException ex) { + addError("Incorrect pattern found", ex); + } + + started = true; + } + + private void postCompileProcessing(Converter c) { + while (c != null) { + if (c instanceof ThrowableHandlingConverter) { + chainHandlesThrowable = true; + } + c = c.getNext(); + } + chainHandlesThrowable = false; + } + + /** + * The <b>Title </b> option takes a String value. This option sets the + * document title of the generated HTML document. + * + * <p> + * Defaults to 'Logback Log Messages'. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Returns the current value of the <b>Title </b> option. + */ + public String getTitle() { + return title; + } + + /** + * Returns the value of the internalCSS option. See {@link #setInternalCSS} + * method for details about the meaning of this option. + * + * @return boolean Value of internalCSS option + */ + public boolean isInternalCSS() { + return internalCSS; + } + + /** + * Set the value of the internalCSS option. If set to true, the generated HTML + * ouput will include an internal cascading style sheet. Otherwise, the + * generated HTML output will include a reference to an external CSS. + * <p> + * By default, <code>internalCSS</code> value is set to false, that is, by + * default, only a link to an external CSS file will be generated. + * + * @see #setURL2ExternalCSS + * + * @param internalCSS + */ + public void setInternalCSS(boolean internalCSS) { + this.internalCSS = internalCSS; + } + + /** + * Return the URL to the external CSS file. See {@link #setURL2ExternalCSS} + * method for details about the meaning of this option. + * + * @return URL to the external CSS file. + */ + public String getURL2ExternalCSS() { + return url2ExternalCSS; + } + + /** + * Set the URL for the external CSS file. By default, the external CSS file is + * set to "http://logging.apache.org/log4j/docs/css/eventTable-1.0.css". + */ + public void setURL2ExternalCSS(String url2ExternalCSS) { + this.url2ExternalCSS = url2ExternalCSS; + } + + /** + * Returns the content type output by this layout, i.e "text/html". + */ + public String getContentType() { + return "text/html"; + } + + void appendThrowableAsHTML(final String[] s, final StringBuffer sbuf) { + if (s != null) { + int len = s.length; + if (len == 0) { + return; + } + sbuf.append(Transform.escapeTags(s[0])); + sbuf.append(LINE_SEP); + for (int i = 1; i < len; i++) { + sbuf.append(TRACE_PREFIX); + sbuf.append(Transform.escapeTags(s[i])); + sbuf.append(LINE_SEP); + } + } + } + + /** + * Returns appropriate HTML headers. + */ + public String getHeader() { + StringBuffer sbuf = new StringBuffer(); + sbuf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\""); + sbuf.append(" \"http://www.w3.org/TR/html4/loose.dtd\">"); + sbuf.append(LINE_SEP); + sbuf.append("<html>"); + sbuf.append(LINE_SEP); + sbuf.append("<head>"); + sbuf.append(LINE_SEP); + sbuf.append("<title>"); + sbuf.append(title); + sbuf.append("</title>"); + sbuf.append(LINE_SEP); + if (internalCSS) { + getInternalCSS(sbuf); + } else { + sbuf.append("<LINK REL=StyleSheet HREF=\""); + sbuf.append(url2ExternalCSS); + sbuf.append("\" TITLE=\"Basic\">"); + } + sbuf.append(LINE_SEP); + sbuf.append("</head>"); + sbuf.append(LINE_SEP); + sbuf.append("<body>"); + sbuf.append(LINE_SEP); + + sbuf.append("<hr size=\"1\" noshade>"); + sbuf.append(LINE_SEP); + + sbuf.append("Log session start time "); + sbuf.append(new java.util.Date()); + sbuf.append("<br>"); + sbuf.append(LINE_SEP); + sbuf.append("<br>"); + sbuf.append(LINE_SEP); + sbuf.append("<table cellspacing=\"0\">"); + sbuf.append(LINE_SEP); + + sbuf.append("<tr class=\"header\">"); + sbuf.append(LINE_SEP); + + Converter c = head; + String name; + while (c != null) { + name = computeConverterName(c); + if (name == null) { + c = c.getNext(); + continue; + } + sbuf.append("<td class=\""); + sbuf.append(computeConverterName(c)); + sbuf.append("\">"); + sbuf.append("<td>"); + sbuf.append(computeConverterName(c)); + sbuf.append("</td>"); + sbuf.append(LINE_SEP); + c = c.getNext(); + } + sbuf.append("</tr>"); + sbuf.append(LINE_SEP); + + return sbuf.toString(); + } + + /** + * Returns the appropriate HTML footers. + */ + public String getFooter() { + StringBuffer sbuf = new StringBuffer(); + sbuf.append("</table>"); + sbuf.append(LINE_SEP); + sbuf.append("<br>"); + sbuf.append(LINE_SEP); + sbuf.append("</body></html>"); + return sbuf.toString(); + } + + /** + * The HTML layout handles the throwable contained in logging events. Hence, + * this method return <code>false</code>. + */ + public boolean ignoresThrowable() { + return false; + } + + public String doLayout(Object event) { + return doLayout((LoggingEvent) event); + } + + public String doLayout(LoggingEvent event) { + + boolean odd = true; + if (((counter++) & 1) == 0) { + odd = false; + } + + String level = event.getLevel().toString().toLowerCase(); + + StringBuffer buf = new StringBuffer(); + buf.append(LINE_SEP); + buf.append("<tr class=\""); + buf.append(level); + if (odd) { + buf.append(" odd\">"); + } else { + buf.append(" even\">"); + } + buf.append(LINE_SEP); + + Converter c = head; + while (c != null) { + if (c instanceof ThrowableHandlingConverter) { + ThrowableHandlingConverter converter = (ThrowableHandlingConverter)c; + if (converter.onNewLine(event)) { + buf.append("</tr>"); + buf.append("<tr>"); + appendEventToBuffer(buf, c, event); + if (c.getNext() != null) { + //here we assume that when we exist the while loop, + //a </tr> tag is added. + buf.append("</tr>"); + buf.append("<tr>"); + } + } + } else { + appendEventToBuffer(buf, c, event); + } + c = c.getNext(); + } + buf.append("</tr>"); + buf.append(LINE_SEP); + + // if the pattern chain handles throwables then no need to do it again here. + if (!chainHandlesThrowable) { + ThrowableInformation ti = event.getThrowableInformation(); + if (ti != null) { + String[] s = ti.getThrowableStrRep(); + if (s != null) { + buf.append("<tr><td class=\"Exception\" colspan=\"6\">"); + appendThrowableAsHTML(s, buf); + buf.append("</td></tr>"); + buf.append(LINE_SEP); + } + } + } + return buf.toString(); + } + + private void appendEventToBuffer(StringBuffer buf, Converter c, LoggingEvent event) { + buf.append("<td class=\""); + buf.append(computeConverterName(c)); + buf.append("\">"); + buf.append(c.convert(event)); + buf.append("</td>"); + buf.append(LINE_SEP); + } + + /** + * Generate an internal CSS file. + * + * @param buf The StringBuffer where the CSS file will be placed. + */ + void getInternalCSS(StringBuffer buf) { + + buf.append("<STYLE type=\"text/css\">"); + buf.append(LINE_SEP); + buf.append("table { margin-left: 2em; margin-right: 2em; border-left: 2px solid #AAA; }"); + buf.append(LINE_SEP); + + buf.append("TR.even { background: #FFFFFF; }"); + buf.append(LINE_SEP); + + buf.append("TR.odd { background: #DADADA; }"); + buf.append(LINE_SEP); + + buf.append("TR.warn TD.level, TR.error TD.level, TR.fatal TD.level {font-weight: bold; color: #FF4040 }"); + buf.append(LINE_SEP); + + buf.append("TD { padding-right: 1ex; padding-left: 1ex; border-right: 2px solid #AAA; }"); + buf.append(LINE_SEP); + + buf.append("TD.Time, TD.Date { text-align: right; font-family: courier, monospace; font-size: smaller; }"); + buf.append(LINE_SEP); + + buf.append("TD.Thread { text-align: left; }"); + buf.append(LINE_SEP); + + buf.append("TD.Level { text-align: right; }"); + buf.append(LINE_SEP); + + buf.append("TD.Logger { text-align: left; }"); + buf.append(LINE_SEP); + + buf.append("TR.header { background: #9090FF; color: #FFF; font-weight: bold; font-size: larger; }"); + buf.append(LINE_SEP); + + buf.append("TD.Exception { background: #C0C0F0; font-family: courier, monospace;}"); + buf.append(LINE_SEP); + + buf.append("</STYLE>"); + buf.append(LINE_SEP); + + } + + private String computeConverterName(Converter c) { + String className = c.getClass().getSimpleName(); + int index = className.indexOf("Converter"); + if (index == -1) { + return className; + } else { + return className.substring(0, index); + } + } + +} Modified: logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/net/SMTPAppender.java ============================================================================== --- logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/net/SMTPAppender.java (original) +++ logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/net/SMTPAppender.java Tue Sep 12 09:35:04 2006 @@ -50,342 +50,341 @@ * @since 1.0 */ public class SMTPAppender extends AppenderBase { - private Layout layout; + private Layout layout; - private String to; - private String from; - private String subject; - private String smtpHost; - private int bufferSize = 512; - private boolean locationInfo = false; - - protected CyclicBuffer cb = new CyclicBuffer(bufferSize); - protected Message msg; - - protected TriggeringPolicy evaluator; - - /** - * The default constructor will instantiate the appender with a - * {@link TriggeringEventEvaluator} that will trigger on events with level - * ERROR or higher. - */ - public SMTPAppender() { - this(new DefaultEvaluator()); - } - - /** - * Use <code>evaluator</code> passed as parameter as the {@link - * TriggeringEventEvaluator} for this SMTPAppender. - */ - public SMTPAppender(TriggeringPolicy evaluator) { - this.evaluator = evaluator; - } - - /** - * Start the appender - */ - public void start() { - Properties props = new Properties(System.getProperties()); - if (smtpHost != null) { - props.put("mail.smtp.host", smtpHost); - } - - Session session = Session.getInstance(props, null); - // session.setDebug(true); - msg = new MimeMessage(session); - - try { - if (from != null) { - msg.setFrom(getAddress(from)); - } else { - msg.setFrom(); - } - - msg.setRecipients(Message.RecipientType.TO, parseAddress(to)); - if (subject != null) { - msg.setSubject(subject); - } - - started = true; - - } catch (MessagingException e) { - addError("Could not activate SMTPAppender options.", e); - } - } - - /** - * Perform SMTPAppender specific appending actions, mainly adding the event to - * a cyclic buffer and checking if the event triggers an e-mail to be sent. - */ - protected void append(Object eventObject) { - LoggingEvent event = (LoggingEvent) eventObject; - - if (!checkEntryConditions()) { - return; - } - - event.getThreadName(); - // event.getNDC(); - // if (locationInfo) { - // event.getLocationInformation(); - // } - cb.add(event); - //addInfo("Added event to the cyclic buffer: " + event.getMessage()); - - if (evaluator.isTriggeringEvent(null, event)) { - sendBuffer(); - } - } - - /** - * This method determines if there is a sense in attempting to append. - * - * <p> - * It checks whether there is a set output target and also if there is a set - * layout. If these checks fail, then the boolean value <code>false</code> - * is returned. - */ - protected boolean checkEntryConditions() { - if (this.msg == null) { - addError("Message object not configured."); - return false; - } - - if (this.evaluator == null) { - addError("No TriggeringPolicy is set for appender [" + name + "]."); - return false; - } - - if (this.layout == null) { - addError("No layout set for appender named [" + name + "]."); - return false; - } - return true; - } - - synchronized public void stop() { - this.started = false; - } - - InternetAddress getAddress(String addressStr) { - try { - return new InternetAddress(addressStr); - } catch (AddressException e) { - addError("Could not parse address [" + addressStr + "].", e); - return null; - } - } - - InternetAddress[] parseAddress(String addressStr) { - try { - return InternetAddress.parse(addressStr, true); - } catch (AddressException e) { - addError("Could not parse address [" + addressStr + "].", e); - return null; - } - } - - /** - * Returns value of the <b>To</b> option. - */ - public String getTo() { - return to; - } - - /** - * Send the contents of the cyclic buffer as an e-mail message. - */ - protected void sendBuffer() { - - // Note: this code already owns the monitor for this - // appender. This frees us from needing to synchronize on 'cb'. - try { - MimeBodyPart part = new MimeBodyPart(); - - StringBuffer sbuf = new StringBuffer(); - String t = layout.getHeader(); - if (t != null) - sbuf.append(t); - int len = cb.length(); - for (int i = 0; i < len; i++) { - //sbuf.append(MimeUtility.encodeText(layout.format(cb.get()))); - LoggingEvent event = cb.get(); - sbuf.append(layout.doLayout(event)); - // if (layout.ignoresThrowable()) { - // String[] s = event.getThrowableStrRep(); - // if (s != null) { - // for (int j = 0; j < s.length; j++) { - // sbuf.append(s[j]); - // } - // } - // } - } - t = layout.getFooter(); - if (t != null) - sbuf.append(t); - part.setContent(sbuf.toString(), "text/plain"); - - Multipart mp = new MimeMultipart(); - mp.addBodyPart(part); - msg.setContent(mp); - - msg.setSentDate(new Date()); - Transport.send(msg); - } catch (Exception e) { - addError("Error occured while sending e-mail notification.", e); - } - } - - /** - * Returns value of the <b>EvaluatorClass</b> option. - */ - public String getEvaluatorClass() { - return evaluator == null ? null : evaluator.getClass().getName(); - } - - /** - * Returns value of the <b>From</b> option. - */ - public String getFrom() { - return from; - } - - /** - * Returns value of the <b>Subject</b> option. - */ - public String getSubject() { - return subject; - } - - /** - * The <b>From</b> option takes a string value which should be a e-mail - * address of the sender. - */ - public void setFrom(String from) { - this.from = from; - } - - /** - * The <b>Subject</b> option takes a string value which should be a the - * subject of the e-mail message. - */ - public void setSubject(String subject) { - this.subject = subject; - } - - /** - * The <b>BufferSize</b> option takes a positive integer representing the - * maximum number of logging events to collect in a cyclic buffer. When the - * <code>BufferSize</code> is reached, oldest events are deleted as new - * events are added to the buffer. By default the size of the cyclic buffer is - * 512 events. - */ - public void setBufferSize(int bufferSize) { - this.bufferSize = bufferSize; - cb.resize(bufferSize); - } - - /** - * The <b>SMTPHost</b> option takes a string value which should be a the host - * name of the SMTP server that will send the e-mail message. - */ - public void setSMTPHost(String smtpHost) { - this.smtpHost = smtpHost; - } - - /** - * Returns value of the <b>SMTPHost</b> option. - */ - public String getSMTPHost() { - return smtpHost; - } - - /** - * The <b>To</b> option takes a string value which should be a comma - * separated list of e-mail address of the recipients. - */ - public void setTo(String to) { - this.to = to; - } - - /** - * Returns value of the <b>BufferSize</b> option. - */ - public int getBufferSize() { - return bufferSize; - } - - /** - * The <b>EvaluatorClass</b> option takes a string value representing the - * name of the class implementing the {@link TriggeringEventEvaluator} - * interface. A corresponding object will be instantiated and assigned as the - * triggering event evaluator for the SMTPAppender. - */ - public void setEvaluatorClass(String value) { - try { - evaluator = (TriggeringPolicy) OptionHelper.instantiateByClassName(value, - TriggeringPolicy.class); - } catch (Exception ex) { - addError("Evaluator class instanciation failed"); - } - } - - /** - * The <b>LocationInfo</b> option takes a boolean value. By default, it is - * set to false which means there will be no effort to extract the location - * information related to the event. As a result, the layout that formats the - * events as they are sent out in an e-mail is likely to place the wrong - * location information (if present in the format). - * - * <p> - * Location information extraction is comparatively very slow and should be - * avoided unless performance is not a concern. - */ - public void setLocationInfo(boolean locationInfo) { - this.locationInfo = locationInfo; - } - - /** - * Returns value of the <b>LocationInfo</b> option. - */ - public boolean getLocationInfo() { - return locationInfo; - } - - public Layout getLayout() { - return layout; - } - - public void setLayout(Layout layout) { - this.layout = layout; - } + private String to; + private String from; + private String subject; + private String smtpHost; + private int bufferSize = 512; + private boolean locationInfo = false; + + protected CyclicBuffer cb = new CyclicBuffer(bufferSize); + protected Message msg; + + protected TriggeringPolicy evaluator; + + /** + * The default constructor will instantiate the appender with a + * {@link TriggeringEventEvaluator} that will trigger on events with level + * ERROR or higher. + */ + public SMTPAppender() { + this(new DefaultEvaluator()); + } + + /** + * Use <code>evaluator</code> passed as parameter as the {@link + * TriggeringEventEvaluator} for this SMTPAppender. + */ + public SMTPAppender(TriggeringPolicy evaluator) { + this.evaluator = evaluator; + } + + /** + * Start the appender + */ + public void start() { + Properties props = new Properties(System.getProperties()); + if (smtpHost != null) { + props.put("mail.smtp.host", smtpHost); + } + + Session session = Session.getInstance(props, null); + // session.setDebug(true); + msg = new MimeMessage(session); + + try { + if (from != null) { + msg.setFrom(getAddress(from)); + } else { + msg.setFrom(); + } + + msg.setRecipients(Message.RecipientType.TO, parseAddress(to)); + if (subject != null) { + msg.setSubject(subject); + } + + started = true; + + } catch (MessagingException e) { + addError("Could not activate SMTPAppender options.", e); + } + } + + /** + * Perform SMTPAppender specific appending actions, mainly adding the event to + * a cyclic buffer and checking if the event triggers an e-mail to be sent. + */ + protected void append(Object eventObject) { + LoggingEvent event = (LoggingEvent) eventObject; + + if (!checkEntryConditions()) { + return; + } + + event.getThreadName(); + // if (locationInfo) { + // event.getLocationInformation(); + // } + cb.add(event); + // addInfo("Added event to the cyclic buffer: " + event.getMessage()); + + if (evaluator.isTriggeringEvent(null, event)) { + sendBuffer(); + } + } + + /** + * This method determines if there is a sense in attempting to append. + * + * <p> + * It checks whether there is a set output target and also if there is a set + * layout. If these checks fail, then the boolean value <code>false</code> + * is returned. + */ + protected boolean checkEntryConditions() { + if (this.msg == null) { + addError("Message object not configured."); + return false; + } + + if (this.evaluator == null) { + addError("No TriggeringPolicy is set for appender [" + name + "]."); + return false; + } + + if (this.layout == null) { + addError("No layout set for appender named [" + name + "]."); + return false; + } + return true; + } + + synchronized public void stop() { + this.started = false; + } + + InternetAddress getAddress(String addressStr) { + try { + return new InternetAddress(addressStr); + } catch (AddressException e) { + addError("Could not parse address [" + addressStr + "].", e); + return null; + } + } + + InternetAddress[] parseAddress(String addressStr) { + try { + return InternetAddress.parse(addressStr, true); + } catch (AddressException e) { + addError("Could not parse address [" + addressStr + "].", e); + return null; + } + } + + /** + * Returns value of the <b>To</b> option. + */ + public String getTo() { + return to; + } + + /** + * Send the contents of the cyclic buffer as an e-mail message. + */ + protected void sendBuffer() { + + // Note: this code already owns the monitor for this + // appender. This frees us from needing to synchronize on 'cb'. + try { + MimeBodyPart part = new MimeBodyPart(); + + StringBuffer sbuf = new StringBuffer(); + String t = layout.getHeader(); + if (t != null) + sbuf.append(t); + int len = cb.length(); + for (int i = 0; i < len; i++) { + // sbuf.append(MimeUtility.encodeText(layout.format(cb.get()))); + LoggingEvent event = cb.get(); + sbuf.append(layout.doLayout(event)); + // if (layout.ignoresThrowable()) { + // String[] s = event.getThrowableStrRep(); + // if (s != null) { + // for (int j = 0; j < s.length; j++) { + // sbuf.append(s[j]); + // } + // } + // } + } + t = layout.getFooter(); + if (t != null) + sbuf.append(t); + part.setContent(sbuf.toString(), "text/plain"); + + Multipart mp = new MimeMultipart(); + mp.addBodyPart(part); + msg.setContent(mp); + + msg.setSentDate(new Date()); + Transport.send(msg); + } catch (Exception e) { + addError("Error occured while sending e-mail notification.", e); + } + } + + /** + * Returns value of the <b>EvaluatorClass</b> option. + */ + public String getEvaluatorClass() { + return evaluator == null ? null : evaluator.getClass().getName(); + } + + /** + * Returns value of the <b>From</b> option. + */ + public String getFrom() { + return from; + } + + /** + * Returns value of the <b>Subject</b> option. + */ + public String getSubject() { + return subject; + } + + /** + * The <b>From</b> option takes a string value which should be a e-mail + * address of the sender. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * The <b>Subject</b> option takes a string value which should be a the + * subject of the e-mail message. + */ + public void setSubject(String subject) { + this.subject = subject; + } + + /** + * The <b>BufferSize</b> option takes a positive integer representing the + * maximum number of logging events to collect in a cyclic buffer. When the + * <code>BufferSize</code> is reached, oldest events are deleted as new + * events are added to the buffer. By default the size of the cyclic buffer is + * 512 events. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + cb.resize(bufferSize); + } + + /** + * The <b>SMTPHost</b> option takes a string value which should be a the host + * name of the SMTP server that will send the e-mail message. + */ + public void setSMTPHost(String smtpHost) { + this.smtpHost = smtpHost; + } + + /** + * Returns value of the <b>SMTPHost</b> option. + */ + public String getSMTPHost() { + return smtpHost; + } + + /** + * The <b>To</b> option takes a string value which should be a comma + * separated list of e-mail address of the recipients. + */ + public void setTo(String to) { + this.to = to; + } + + /** + * Returns value of the <b>BufferSize</b> option. + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * The <b>EvaluatorClass</b> option takes a string value representing the + * name of the class implementing the {@link TriggeringEventEvaluator} + * interface. A corresponding object will be instantiated and assigned as the + * triggering event evaluator for the SMTPAppender. + */ + public void setEvaluatorClass(String value) { + try { + evaluator = (TriggeringPolicy) OptionHelper.instantiateByClassName(value, + TriggeringPolicy.class); + } catch (Exception ex) { + addError("Evaluator class instanciation failed"); + } + } + + /** + * The <b>LocationInfo</b> option takes a boolean value. By default, it is + * set to false which means there will be no effort to extract the location + * information related to the event. As a result, the layout that formats the + * events as they are sent out in an e-mail is likely to place the wrong + * location information (if present in the format). + * + * <p> + * Location information extraction is comparatively very slow and should be + * avoided unless performance is not a concern. + */ + public void setLocationInfo(boolean locationInfo) { + this.locationInfo = locationInfo; + } + + /** + * Returns value of the <b>LocationInfo</b> option. + */ + public boolean getLocationInfo() { + return locationInfo; + } + + public Layout getLayout() { + return layout; + } + + public void setLayout(Layout layout) { + this.layout = layout; + } } class DefaultEvaluator implements TriggeringPolicy { - private boolean started; + private boolean started; - /** - * Is this <code>event</code> the e-mail triggering event? - * - * <p> - * This method returns <code>true</code>, if the event level has ERROR - * level or higher. Otherwise it returns <code>false</code>. - */ - public boolean isTriggeringEvent(File file, Object eventObject) { - LoggingEvent event = (LoggingEvent) eventObject; - return event.getLevel().isGreaterOrEqual(Level.ERROR); - } - - public boolean isStarted() { - return started == true; - } - - public void start() { - started = true; - } - - public void stop() { - started = false; - } + /** + * Is this <code>event</code> the e-mail triggering event? + * + * <p> + * This method returns <code>true</code>, if the event level has ERROR + * level or higher. Otherwise it returns <code>false</code>. + */ + public boolean isTriggeringEvent(File file, Object eventObject) { + LoggingEvent event = (LoggingEvent) eventObject; + return event.getLevel().isGreaterOrEqual(Level.ERROR); + } + + public boolean isStarted() { + return started == true; + } + + public void start() { + started = true; + } + + public void stop() { + started = false; + } } Modified: logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableHandlingConverter.java ============================================================================== --- logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableHandlingConverter.java (original) +++ logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableHandlingConverter.java Tue Sep 12 09:35:04 2006 @@ -1,6 +1,7 @@ package ch.qos.logback.classic.pattern; import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.LoggingEvent; /** @@ -12,4 +13,9 @@ boolean handlesThrowable() { return true; } + + // tentatively... + public boolean onNewLine(LoggingEvent le) { + return le.getThrowableInformation() != null; + } } Modified: logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java ============================================================================== --- logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java (original) +++ logback/trunk/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java Tue Sep 12 09:35:04 2006 @@ -41,247 +41,247 @@ */ public class LoggingEvent implements Serializable { - /** - * - */ - private static final long serialVersionUID = 3022264832697160750L; - - /** - * - */ - private static long startTime = System.currentTimeMillis(); - - /** - * Fully qualified name of the calling Logger class. This field does not - * survive serialization. - * - * <p> - * Note that the getCallerInformation() method relies on this fact. - */ - transient String fqnOfLoggerClass; - - /** - * The name of thread in which this logging event was generated. - */ - private String threadName; - - /** - * Level of logging event. - * - * <p> - * This field should not be accessed directly. You shoud use the {@link - * #getLevel} method instead. - * </p> - * - */ - private transient Level level; - - private String message; - private String formattedMessage; - - private Object[] argumentArray; - - private ThrowableInformation throwableInfo; - - private CallerData[] callerDataArray; - private LoggerRemoteView loggerRemoteView; - - private Marker marker; - - private Map<String, String> mdcPropertyMap; - - /** - * The number of milliseconds elapsed from 1/1/1970 until logging event was - * created. - */ - private long timeStamp; - - public LoggingEvent() { - } - - public LoggingEvent(String fqcn, Logger logger, Level level, String message, - Throwable throwable, Object[] argArray) { - this.fqnOfLoggerClass = fqcn; - this.loggerRemoteView = logger.getLoggerRemoteView(); - this.level = level; - this.message = message; - - if (throwable != null) { - this.throwableInfo = new ThrowableInformation(throwable); - } - - if (argArray != null) { - formattedMessage = MessageFormatter.arrayFormat(message, argArray); - } else { - formattedMessage = message; - } - timeStamp = System.currentTimeMillis(); - - mdcPropertyMap = MDC.getPropertyMap(); - } - - public void setArgumentArray(Object[] argArray) { - if (this.argumentArray != null) { - throw new IllegalStateException("argArray has been already set"); - } - this.argumentArray = argArray; - } - - public Object[] getArgumentArray() { - return this.argumentArray; - } - - public Level getLevel() { - return level; - } - - public String getThreadName() { - if (threadName == null) { - threadName = (Thread.currentThread()).getName(); - } - return threadName; - } - - /** - * @param threadName - * The threadName to set. - * @throws IllegalStateException - * If threadName has been already set. - */ - public void setThreadName(String threadName) throws IllegalStateException { - if (this.threadName != null) { - throw new IllegalStateException("threadName has been already set"); - } - this.threadName = threadName; - } - - /** - * Returns the throwable information contained within this event. May be - * <code>null</code> if there is no such information. - */ - public ThrowableInformation getThrowableInformation() { - return throwableInfo; - } - - /** - * Set this event's throwable information. - */ - public void setThrowableInformation(ThrowableInformation ti) { - if (throwableInfo != null) { - throw new IllegalStateException( - "ThrowableInformation has been already set."); - } else { - throwableInfo = ti; - } - } - - /** - * This method should be called prior to serializing an event. It should also - * be called when using asynchronous logging. - */ - public void prepareForDeferredProcessing() { - this.getThreadName(); - } - - public LoggerRemoteView getLoggerRemoteView() { - return loggerRemoteView; - } - - public void setLoggerRemoteView(LoggerRemoteView loggerRemoteView) { - this.loggerRemoteView = loggerRemoteView; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - if (this.message != null) { - throw new IllegalStateException( - "The message for this event has been set already."); - } - this.message = message; - } - - public long getTimeStamp() { - return timeStamp; - } - - public void setTimeStamp(long timeStamp) { - this.timeStamp = timeStamp; - } - - public void setLevel(Level level) { - if (this.level != null) { - throw new IllegalStateException( - "The level has been already set for this event."); - } - this.level = level; - } - - /** - * The time at which this class was loaded into memory, expressed in - * millisecond elapsed since the epoch (1.1.1970). - * - * @return The time as measured when this class was loaded into memory. - */ - public static final long getStartTime() { - return startTime; - } - - /** - * Get the caller information for this logging event. If caller information is - * null at the time of its invocation, this method extracts location - * information. The collected information is cached for future use. - * - * <p> - * Note that after serialization it is impossible to correctly extract caller - * information. - * </p> - */ - public CallerData[] getCallerData() { - // we rely on the fact that fqnOfLoggerClass does not survive - // serialization - if (callerDataArray == null && fqnOfLoggerClass != null) { - callerDataArray = CallerData.extract(new Throwable(), fqnOfLoggerClass); - } - return callerDataArray; - } - - public void setCallerInformation(CallerData[] callerDataArray) { - this.callerDataArray = callerDataArray; - } - - public Marker getMarker() { - return marker; - } - - public void setMarker(Marker marker) { - if (this.marker != null) { - throw new IllegalStateException( - "The marker has been already set for this event."); - } - this.marker = marker; - } - - public String getFormattedMessage() { - return formattedMessage; - } - - public Map<String, String> getMDCPropertyMap() { - return mdcPropertyMap; - } - - private void writeObject(ObjectOutputStream out) throws IOException { - out.defaultWriteObject(); - out.writeInt(level.levelInt); - } - - private void readObject(ObjectInputStream in) throws IOException, - ClassNotFoundException { - in.defaultReadObject(); - int levelInt = in.readInt(); - level = Level.toLevel(levelInt); - } + /** + * + */ + private static final long serialVersionUID = 3022264832697160750L; + + /** + * + */ + private static long startTime = System.currentTimeMillis(); + + /** + * Fully qualified name of the calling Logger class. This field does not + * survive serialization. + * + * <p> + * Note that the getCallerInformation() method relies on this fact. + */ + transient String fqnOfLoggerClass; + + /** + * The name of thread in which this logging event was generated. + */ + private String threadName; + + /** + * Level of logging event. + * + * <p> + * This field should not be accessed directly. You shoud use the {@link + * #getLevel} method instead. + * </p> + * + */ + private transient Level level; + + private String message; + private String formattedMessage; + + private Object[] argumentArray; + + private ThrowableInformation throwableInfo; + + private CallerData[] callerDataArray; + private LoggerRemoteView loggerRemoteView; + + private Marker marker; + + private Map<String, String> mdcPropertyMap; + + /** + * The number of milliseconds elapsed from 1/1/1970 until logging event was + * created. + */ + private long timeStamp; + + public LoggingEvent() { + } + + public LoggingEvent(String fqcn, Logger logger, Level level, String message, + Throwable throwable, Object[] argArray) { + this.fqnOfLoggerClass = fqcn; + this.loggerRemoteView = logger.getLoggerRemoteView(); + this.level = level; + this.message = message; + + if (throwable != null) { + this.throwableInfo = new ThrowableInformation(throwable); + } + + if (argArray != null) { + formattedMessage = MessageFormatter.arrayFormat(message, argArray); + } else { + formattedMessage = message; + } + timeStamp = System.currentTimeMillis(); + + mdcPropertyMap = MDC.getPropertyMap(); + } + + public void setArgumentArray(Object[] argArray) { + if (this.argumentArray != null) { + throw new IllegalStateException("argArray has been already set"); + } + this.argumentArray = argArray; + } + + public Object[] getArgumentArray() { + return this.argumentArray; + } + + public Level getLevel() { + return level; + } + + public String getThreadName() { + if (threadName == null) { + threadName = (Thread.currentThread()).getName(); + } + return threadName; + } + + /** + * @param threadName + * The threadName to set. + * @throws IllegalStateException + * If threadName has been already set. + */ + public void setThreadName(String threadName) throws IllegalStateException { + if (this.threadName != null) { + throw new IllegalStateException("threadName has been already set"); + } + this.threadName = threadName; + } + + /** + * Returns the throwable information contained within this event. May be + * <code>null</code> if there is no such information. + */ + public ThrowableInformation getThrowableInformation() { + return throwableInfo; + } + + /** + * Set this event's throwable information. + */ + public void setThrowableInformation(ThrowableInformation ti) { + if (throwableInfo != null) { + throw new IllegalStateException( + "ThrowableInformation has been already set."); + } else { + throwableInfo = ti; + } + } + + /** + * This method should be called prior to serializing an event. It should also + * be called when using asynchronous logging. + */ + public void prepareForDeferredProcessing() { + this.getThreadName(); + } + + public LoggerRemoteView getLoggerRemoteView() { + return loggerRemoteView; + } + + public void setLoggerRemoteView(LoggerRemoteView loggerRemoteView) { + this.loggerRemoteView = loggerRemoteView; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + if (this.message != null) { + throw new IllegalStateException( + "The message for this event has been set already."); + } + this.message = message; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + public void setLevel(Level level) { + if (this.level != null) { + throw new IllegalStateException( + "The level has been already set for this event."); + } + this.level = level; + } + + /** + * The time at which this class was loaded into memory, expressed in + * millisecond elapsed since the epoch (1.1.1970). + * + * @return The time as measured when this class was loaded into memory. + */ + public static final long getStartTime() { + return startTime; + } + + /** + * Get the caller information for this logging event. If caller information is + * null at the time of its invocation, this method extracts location + * information. The collected information is cached for future use. + * + * <p> + * Note that after serialization it is impossible to correctly extract caller + * information. + * </p> + */ + public CallerData[] getCallerData() { + // we rely on the fact that fqnOfLoggerClass does not survive + // serialization + if (callerDataArray == null && fqnOfLoggerClass != null) { + callerDataArray = CallerData.extract(new Throwable(), fqnOfLoggerClass); + } + return callerDataArray; + } + + public void setCallerInformation(CallerData[] callerDataArray) { + this.callerDataArray = callerDataArray; + } + + public Marker getMarker() { + return marker; + } + + public void setMarker(Marker marker) { + if (this.marker != null) { + throw new IllegalStateException( + "The marker has been already set for this event."); + } + this.marker = marker; + } + + public String getFormattedMessage() { + return formattedMessage; + } + + public Map<String, String> getMDCPropertyMap() { + return mdcPropertyMap; + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + out.writeInt(level.levelInt); + } + + private void readObject(ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + int levelInt = in.readInt(); + level = Level.toLevel(levelInt); + } } Added: logback/trunk/logback-classic/src/test/java/ch/qos/logback/classic/html/HTMLLayoutTest.java ============================================================================== --- (empty file) +++ logback/trunk/logback-classic/src/test/java/ch/qos/logback/classic/html/HTMLLayoutTest.java Tue Sep 12 09:35:04 2006 @@ -0,0 +1,158 @@ +package ch.qos.logback.classic.html; + +import junit.framework.TestCase; + +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.classic.spi.ThrowableInformation; +import ch.qos.logback.core.appender.ListAppender; + +public class HTMLLayoutTest extends TestCase { + + LoggerContext lc; + Logger logger; + HTMLLayout layout; + + @Override + public void setUp() throws Exception { + super.setUp(); + lc = new LoggerContext(); + lc.setName("default"); + + ListAppender appender = new ListAppender(); + appender.setContext(lc); + layout = new HTMLLayout(); + layout.setContext(lc); + layout.setPattern("%level %thread %msg"); + layout.start(); + appender.setLayout(layout); + logger = lc.getLogger(LoggerContext.ROOT_NAME); + logger.addAppender(appender); + appender.start(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + lc = null; + layout = null; + } + + public void testHeader() throws Exception { + String header = layout.getHeader(); + // System.out.println(header); + assertTrue(header.indexOf("Level") == 422); + assertTrue(header.indexOf("Literal") == 456); + assertTrue(header.indexOf("Thread") == 494); + assertTrue(header.lastIndexOf("Literal") == 543); + assertTrue(header.indexOf("Message") == 139); + } + + public void testAppendThrowable() throws Exception { + StringBuffer buf = new StringBuffer(); + String[] strArray = { "test1", "test2" }; + layout.appendThrowableAsHTML(strArray, buf); + // System.out.println(buf.toString()); + String[] result = buf.toString().split(HTMLLayout.LINE_SEP); + assertEquals("test1", result[0]); + assertEquals(HTMLLayout.TRACE_PREFIX + "test2", result[1]); + } + + public void testDoLayout() throws Exception { + LoggingEvent le = createLoggingEvent(); + String result = layout.doLayout(le); + Document doc = parseOutput(result); + Element trElement = doc.getRootElement(); + assertEquals(5, trElement.elements().size()); + { + Element tdElement = (Element) trElement.elements().get(0); + assertEquals("DEBUG", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(1); + assertEquals(" ", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(2); + assertEquals("main", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(3); + assertEquals(" ", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(4); + assertEquals("test message", tdElement.getText()); + } + // System.out.println(result); + } + + public void testDoLayoutWithException() throws Exception { + layout.setPattern("%level %thread %msg %ex"); + LoggingEvent le = createLoggingEvent(); + le.setThrowableInformation(new ThrowableInformation(new Exception( + "test Exception"))); + String result = layout.doLayout(le); + + //System.out.println(result); + + Document doc = parseOutput(result); + Element trElement = doc.getRootElement(); + assertEquals(6, trElement.elements().size()); + { + Element tdElement = (Element) trElement.elements().get(0); + assertEquals("DEBUG", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(1); + assertEquals(" ", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(2); + assertEquals("main", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(3); + assertEquals(" ", tdElement.getText()); + } + { + Element tdElement = (Element) trElement.elements().get(4); + assertEquals("test message", tdElement.getText()); + } +// { +// Element trElement2 = (Element) trElement.elements().get(5); +// Element tdElement = (Element) trElement2.elements().get(0); +// assertTrue(tdElement.getText().contains( +// "java.lang.Exception: test Exception")); +// } + } + + public void testLog() { + for (int i = 0; i < 2000; i++) { + logger.debug("test message"); + } + } + + private LoggingEvent createLoggingEvent() { + LoggingEvent le = new LoggingEvent(this.getClass().getName(), logger, + Level.DEBUG, "test message", null, null); + return le; + } + + Document parseOutput(String output) { + try { + Document document = DocumentHelper.parseText(output); + return document; + } catch (Exception e) { + System.err.println(e); + fail(); + } + return null; + } +} Modified: logback/trunk/pom.xml ============================================================================== --- logback/trunk/pom.xml (original) +++ logback/trunk/pom.xml Tue Sep 12 09:35:04 2006 @@ -64,7 +64,11 @@ <artifactId>mail</artifactId> <version>1.4</version> </dependency> - + <dependency> + <groupId>dom4j</groupId> + <artifactId>dom4j</artifactId> + <version>1.6.1</version> + </dependency> <!-- Access Module Dependencies --> <dependency>