// NAnt - A .NET build tool
// Copyright (C) 2001-2003 Gerry Shaw
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// William E. Caputo (wecaputo@thoughtworks.com | logosity@yahoo.com)
// Gert Driesen (gert.driesen@ardatis.com)
using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using NAnt.Core.Util;
namespace NAnt.Core {
///
/// Used to wrap log messages in xml <message/> elements.
///
[Serializable()]
public class XmlLogger : IBuildLogger, ISerializable {
private readonly StopWatchStack _stopWatchStack;
#region Private Instance Fields
private TextWriter _outputWriter;
private StringWriter _buffer = new StringWriter();
private Level _threshold = Level.Info;
[NonSerialized()]
private XmlTextWriter _xmlWriter;
///
/// Holds the stack of currently executing projects.
///
private Stack _projectStack = new Stack();
#endregion Private Instance Fields
#region Public Instance Constructors
///
/// Initializes a new instance of the class.
///
public XmlLogger() : this(new StopWatchStack(new DateTimeProvider())) {}
public XmlLogger(StopWatchStack stopWatchStack) {
_xmlWriter = new XmlTextWriter(_buffer);
_stopWatchStack = stopWatchStack;
}
#endregion Public Instance Constructors
#region Protected Instance Constructors
///
/// Initializes a new instance of the class
/// with serialized data.
///
/// The that holds the serialized object data.
/// The that contains contextual information about the source or destination.
protected XmlLogger(SerializationInfo info, StreamingContext context) {
_outputWriter = info.GetValue("OutputWriter", typeof (TextWriter)) as TextWriter;
_buffer = info.GetValue("Buffer", typeof (StringWriter)) as StringWriter;
_threshold = (Level) info.GetValue("Threshold", typeof (Level));
_xmlWriter = new XmlTextWriter(_buffer);
_projectStack = (Stack) info.GetValue("ProjectStack", typeof (Stack));
}
#endregion Protected Instance Constructors
#region Implementation of ISerializable
///
/// Populates with the data needed to serialize
/// the instance.
///
/// The to populate with data.
/// The destination for this serialization.
public void GetObjectData(SerializationInfo info, StreamingContext context) {
info.AddValue("OutputWriter", _outputWriter);
info.AddValue("Buffer", _buffer);
info.AddValue("Threshold", _threshold);
info.AddValue("ProjectStack", _projectStack);
}
#endregion Implementation of ISerializable
#region Override implementation of Object
///
/// Returns the contents of log captured.
///
public override string ToString() {
return _buffer.ToString();
}
#endregion Override implementation of Object
#region Implementation of IBuildListener
///
/// Signals that a build has started.
///
/// The source of the event.
/// A object that contains the event data.
///
/// This event is fired before any targets have started.
///
public void BuildStarted(object sender, BuildEventArgs e) {
lock (_xmlWriter) {
_stopWatchStack.PushStart();
_xmlWriter.WriteStartElement(Elements.BuildResults);
_xmlWriter.WriteAttributeString(Attributes.Project, e.Project.ProjectName);
}
// add an item to the project stack
_projectStack.Push(null);
}
///
/// Signals that the last target has finished.
///
/// The source of the event.
/// A object that contains the event data.
///
/// This event will still be fired if an error occurred during the build.
///
public void BuildFinished(object sender, BuildEventArgs e) {
lock (_xmlWriter) {
if (e.Exception != null) {
_xmlWriter.WriteStartElement("failure");
WriteErrorNode(e.Exception);
_xmlWriter.WriteEndElement();
}
// output total build duration
WriteDuration();
// close buildresults node
_xmlWriter.WriteEndElement();
_xmlWriter.Flush();
}
// remove an item from the project stack
_projectStack.Pop();
// check if there are still nested projects executing
if (_projectStack.Count != 0) {
// do not yet persist build results, as the main project is
// not finished yet
return;
}
try {
// write results to file
if (OutputWriter != null) {
OutputWriter.Write(_buffer.ToString());
OutputWriter.Flush();
}
else { // Xmlogger is used as BuildListener
string outFileName = e.Project.Properties["XmlLogger.file"];
if (outFileName == null) {
outFileName = "log.xml";
}
// convert to full path relative to project base directory
outFileName = e.Project.GetFullPath(outFileName);
// write build log to file
using (StreamWriter writer = new StreamWriter(new FileStream(outFileName, FileMode.Create, FileAccess.Write, FileShare.Read), Encoding.UTF8)) {
writer.Write(_buffer.ToString());
}
}
}
catch (Exception ex) {
throw new BuildException("Unable to write to log file.", ex);
}
}
///
/// Signals that a target has started.
///
/// The source of the event.
/// A object that contains the event data.
public void TargetStarted(object sender, BuildEventArgs e) {
lock (_xmlWriter) {
_stopWatchStack.PushStart();
_xmlWriter.WriteStartElement(Elements.Target);
WriteNameAttribute(e.Target.Name);
_xmlWriter.Flush();
}
}
///
/// Signals that a target has finished.
///
/// The source of the event.
/// A object that contains the event data.
///
/// This event will still be fired if an error occurred during the build.
///
public void TargetFinished(object sender, BuildEventArgs e) {
lock (_xmlWriter) {
// output total target duration
WriteDuration();
// close target element
_xmlWriter.WriteEndElement();
_xmlWriter.Flush();
}
}
///
/// Signals that a task has started.
///
/// The source of the event.
/// A object that contains the event data.
public void TaskStarted(object sender, BuildEventArgs e) {
lock (_xmlWriter) {
_stopWatchStack.PushStart();
_xmlWriter.WriteStartElement(Elements.Task);
WriteNameAttribute(e.Task.Name);
_xmlWriter.Flush();
}
}
///
/// Signals that a task has finished.
///
/// The source of the event.
/// A object that contains the event data.
///
/// This event will still be fired if an error occurred during the build.
///
public void TaskFinished(object sender, BuildEventArgs e) {
lock (_xmlWriter) {
// output total target duration
WriteDuration();
// close task element
_xmlWriter.WriteEndElement();
_xmlWriter.Flush();
}
}
private void WriteDuration() {
_xmlWriter.WriteElementString("duration", XmlConvert.ToString(_stopWatchStack.PopStop().TotalMilliseconds));
}
///
/// Signals that a message has been logged.
///
/// The source of the event.
/// A object that contains the event data.
///
/// Only messages with a priority higher or equal to the threshold of
/// the logger will actually be output in the build log.
///
public void MessageLogged(object sender, BuildEventArgs e) {
if (e.MessageLevel >= Threshold) {
string rawMessage = StripFormatting(e.Message.Trim());
if (IsJustWhiteSpace(rawMessage)) {
return;
}
lock (_xmlWriter) {
_xmlWriter.WriteStartElement(Elements.Message);
// write message level as attribute
_xmlWriter.WriteAttributeString(Attributes.MessageLevel, e.MessageLevel.ToString(CultureInfo.InvariantCulture));
if (IsValidXml(rawMessage)) {
rawMessage = Regex.Replace(rawMessage, @"<\?.*\?>", string.Empty);
_xmlWriter.WriteRaw(rawMessage);
}
else {
_xmlWriter.WriteCData(StripCData(rawMessage));
}
_xmlWriter.WriteEndElement();
_xmlWriter.Flush();
}
}
}
#endregion Implementation of IBuildListener
#region Implementation of IBuildLogger
///
/// Gets or sets the highest level of message this logger should respond
/// to.
///
///
/// The highest level of message this logger should respond to.
///
///
/// Only messages with a message level higher than or equal to the given
/// level should be written to the log.
///
public Level Threshold {
get { return _threshold; }
set { _threshold = value; }
}
///
/// Gets or sets a value indicating whether to produce emacs (and other
/// editor) friendly output.
///
///
/// as it has no meaning in XML format.
///
public virtual bool EmacsMode {
get { return false; }
set {}
}
///
/// Gets or sets the to which the logger is
/// to send its output.
///
public TextWriter OutputWriter {
get { return _outputWriter; }
set { _outputWriter = value; }
}
///
/// Flushes buffered build events or messages to the underlying storage.
///
public void Flush() {
lock (_xmlWriter) {
_xmlWriter.Flush();
}
}
#endregion Implementation of IBuildLogger
#region Public Instance Methods
public string StripFormatting(string message) {
// will hold the message stripped from whitespace and null characters
string strippedMessage;
// looking for whitespace or null characters from front of line
// followed by one or more of just about anything between [ and ]
// followed by a message which we will capture. eg. ' [blah]
Regex r = new Regex(@"(?ms)^[\s\0]*?\[[\s\w\d]+\](.+)");
Match m = r.Match(message);
if (m.Success) {
strippedMessage = m.Groups[1].Captures[0].Value;
strippedMessage = strippedMessage.Replace("\0", string.Empty);
strippedMessage = strippedMessage.Trim();
}
else {
strippedMessage = message.Replace("\0", string.Empty);
}
return strippedMessage;
}
public bool IsJustWhiteSpace(string message) {
Regex r = new Regex(@"^[\s\0]*$");
return r.Match(message).Success;
}
#endregion Public Instance Methods
#region Private Instance Methods
private void WriteErrorNode(Exception exception) {
// this method assumes that a synchronization
// lock on _xmlWriter is already held
if (exception == null) {
// build success
return;
}
else {
BuildException buildException = null;
if (typeof (BuildException).IsAssignableFrom(exception.GetType())) {
buildException = (BuildException) exception;
}
if (buildException != null) {
// start build error node
_xmlWriter.WriteStartElement("builderror");
}
else {
// start build error node
_xmlWriter.WriteStartElement("internalerror");
}
// write exception type
_xmlWriter.WriteElementString("type", exception.GetType().FullName);
// write location for build exceptions
if (buildException != null) {
// write raw exception message
if (buildException.RawMessage != null) {
_xmlWriter.WriteStartElement("message");
_xmlWriter.WriteCData(StripCData(buildException.RawMessage));
_xmlWriter.WriteEndElement();
}
if (buildException.Location != null) {
if (!StringUtils.IsNullOrEmpty(buildException.Location.ToString())) {
_xmlWriter.WriteStartElement("location");
_xmlWriter.WriteElementString("filename", buildException.Location.FileName);
_xmlWriter.WriteElementString("linenumber",
buildException.Location.LineNumber.ToString(CultureInfo.InvariantCulture));
_xmlWriter.WriteElementString("columnnumber",
buildException.Location.ColumnNumber.ToString(CultureInfo.InvariantCulture));
_xmlWriter.WriteEndElement();
}
}
}
else {
// write exception message
if (exception.Message != null) {
_xmlWriter.WriteStartElement("message");
_xmlWriter.WriteCData(StripCData(exception.Message));
_xmlWriter.WriteEndElement();
}
}
// write stacktrace of exception to build log
_xmlWriter.WriteStartElement("stacktrace");
_xmlWriter.WriteCData(exception.StackTrace);
_xmlWriter.WriteEndElement();
// write information about inner exception
WriteErrorNode(exception.InnerException);
// close failure node
_xmlWriter.WriteEndElement();
}
}
private bool IsValidXml(string message) {
if (Regex.Match(message, @"^<.*>").Success) {
XmlValidatingReader reader = null;
try {
// validate xml
reader = new XmlValidatingReader(message,
XmlNodeType.Document, null);
while (reader.Read()) {
}
// the xml is valid
return true;
} catch {
return false;
} finally {
if (reader != null) {
reader.Close();
}
}
}
return false;
}
private string StripCData(string message) {
string strippedMessage = Regex.Replace(message, @"", string.Empty);
}
private void WriteNameAttribute(string name) {
// this method assumes that a synchronization
// lock on _xmlWriter is already held
_xmlWriter.WriteAttributeString("name", name);
}
#endregion Private Instance Methods
private class Elements {
public const string BuildResults = "buildresults";
public const string Message = "message";
public const string Target = "target";
public const string Task = "task";
public const string Status = "status";
}
private class Attributes {
public const string Project = "project";
public const string MessageLevel = "level";
}
}
}