#region Apache License
//
// 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.
//
#endregion

using System;
using System.Text;
using System.Xml;

using log4net.Core;
using log4net.Util;

namespace log4net.Layout
{
	/// <summary>
	/// Layout that formats the log events as XML elements.
	/// </summary>
	/// <remarks>
	/// <para>
	/// The output of the <see cref="XmlLayout" /> consists of a series of 
	/// log4net:event elements. It does not output a complete well-formed XML 
	/// file. The output is designed to be included as an <em>external entity</em>
	/// in a separate file to form a correct XML file.
	/// </para>
	/// <para>
	/// For example, if <c>abc</c> is the name of the file where
	/// the <see cref="XmlLayout" /> output goes, then a well-formed XML file would 
	/// be:
	/// </para>
	/// <code lang="XML">
	/// &lt;?xml version="1.0" ?&gt;
	/// 
	/// &lt;!DOCTYPE log4net:events SYSTEM "log4net-events.dtd" [&lt;!ENTITY data SYSTEM "abc"&gt;]&gt;
	///
	/// &lt;log4net:events version="1.2" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2&gt;
	///     &amp;data;
	/// &lt;/log4net:events&gt;
	/// </code>
	/// <para>
	/// This approach enforces the independence of the <see cref="XmlLayout" /> 
	/// and the appender where it is embedded.
	/// </para>
	/// <para>
	/// The <c>version</c> attribute helps components to correctly
	/// interpret output generated by <see cref="XmlLayout" />. The value of 
	/// this attribute should be "1.2" for release 1.2 and later.
	/// </para>
	/// <para>
	/// Alternatively the <c>Header</c> and <c>Footer</c> properties can be
	/// configured to output the correct XML header, open tag and close tag.
	/// When setting the <c>Header</c> and <c>Footer</c> properties it is essential
	/// that the underlying data store not be appendable otherwise the data
	/// will become invalid XML.
	/// </para>
	/// </remarks>
	/// <author>Nicko Cadell</author>
	/// <author>Gert Driesen</author>
	public class XmlLayout : XmlLayoutBase
	{
		#region Public Instance Constructors

		/// <summary>
		/// Constructs an XmlLayout
		/// </summary>
		public XmlLayout() : base()
		{
		}

		/// <summary>
		/// Constructs an XmlLayout.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The <b>LocationInfo</b> option takes a boolean value. By
		/// default, it is set to false which means there will be no location
		/// information output by this layout. If the the option is set to
		/// true, then the file name and line number of the statement
		/// at the origin of the log statement will be output. 
		/// </para>
		/// <para>
		/// If you are embedding this layout within an SmtpAppender
		/// then make sure to set the <b>LocationInfo</b> option of that 
		/// appender as well.
		/// </para>
		/// </remarks>
		public XmlLayout(bool locationInfo) :  base(locationInfo)
		{
		}

		#endregion Public Instance Constructors

		#region Public Instance Properties

		/// <summary>
		/// The prefix to use for all element names
		/// </summary>
		/// <remarks>
		/// <para>
		/// The default prefix is <b>log4net</b>. Set this property
		/// to change the prefix. If the prefix is set to an empty string
		/// then no prefix will be written.
		/// </para>
		/// </remarks>
		public string Prefix
		{
			get { return m_prefix; }
			set { m_prefix = value; }
		}

		
		/// <summary>
		/// Set whether or not to base64 encode the message.
		/// </summary>
		/// <remarks>
		/// <para>
		/// By default the log message will be written as text to the xml
		/// output. This can cause problems when the message contains binary
		/// data. By setting this to true the contents of the message will be
		/// base64 encoded. If this is set then invalid character replacement
		/// (see <see cref="XmlLayoutBase.InvalidCharReplacement"/>) will not be performed
		/// on the log message.
		/// </para>
		/// </remarks>
		public bool Base64EncodeMessage
		{
			get {return m_base64Message;}
			set {m_base64Message=value;}
		}

		/// <summary>
		/// Set whether or not to base64 encode the property values.
		/// </summary>
		/// <remarks>
		/// <para>
		/// By default the properties will be written as text to the xml
		/// output. This can cause problems when one or more properties contain
		/// binary data. By setting this to true the values of the properties
		/// will be base64 encoded. If this is set then invalid character replacement
		/// (see <see cref="XmlLayoutBase.InvalidCharReplacement"/>) will not be performed
		/// on the property values.
		/// </para>
		/// </remarks>
		public bool Base64EncodeProperties
		{
			get {return m_base64Properties;}
			set {m_base64Properties=value;}
		}


		#endregion Public Instance Properties

		#region Implementation of IOptionHandler

		/// <summary>
		/// Initialize layout options
		/// </summary>
		/// <remarks>
		/// <para>
		/// This is part of the <see cref="IOptionHandler"/> delayed object
		/// activation scheme. The <see cref="ActivateOptions"/> method must 
		/// be called on this object after the configuration properties have
		/// been set. Until <see cref="ActivateOptions"/> is called this
		/// object is in an undefined state and must not be used. 
		/// </para>
		/// <para>
		/// If any of the configuration properties are modified then 
		/// <see cref="ActivateOptions"/> must be called again.
		/// </para>
		/// <para>
		/// Builds a cache of the element names
		/// </para>
		/// </remarks>
		override public void ActivateOptions() 
		{
			base.ActivateOptions();

			// Cache the full element names including the prefix
			if (m_prefix != null && m_prefix.Length > 0)
			{
				m_elmEvent = m_prefix + ":" + ELM_EVENT;
				m_elmMessage = m_prefix + ":" + ELM_MESSAGE;
				m_elmProperties = m_prefix + ":" + ELM_PROPERTIES;
				m_elmData = m_prefix + ":" + ELM_DATA;
				m_elmException = m_prefix + ":" + ELM_EXCEPTION;
				m_elmLocation = m_prefix + ":" + ELM_LOCATION;
			}
		}

		#endregion Implementation of IOptionHandler

		#region Override implementation of XMLLayoutBase

		/// <summary>
		/// Does the actual writing of the XML.
		/// </summary>
		/// <param name="writer">The writer to use to output the event to.</param>
		/// <param name="loggingEvent">The event to write.</param>
		/// <remarks>
		/// <para>
		/// Override the base class <see cref="XmlLayoutBase.FormatXml"/> method
		/// to write the <see cref="LoggingEvent"/> to the <see cref="XmlWriter"/>.
		/// </para>
		/// </remarks>
		override protected void FormatXml(XmlWriter writer, LoggingEvent loggingEvent)
		{
			writer.WriteStartElement(m_elmEvent);
			writer.WriteAttributeString(ATTR_LOGGER, loggingEvent.LoggerName);

#if NET_2_0 || NETCF_2_0 || MONO_2_0 || NET_3_5 || NET_4_0
			writer.WriteAttributeString(ATTR_TIMESTAMP, XmlConvert.ToString(loggingEvent.TimeStamp, XmlDateTimeSerializationMode.Local));
#else
			writer.WriteAttributeString(ATTR_TIMESTAMP, XmlConvert.ToString(loggingEvent.TimeStamp));
#endif

			writer.WriteAttributeString(ATTR_LEVEL, loggingEvent.Level.DisplayName);
			writer.WriteAttributeString(ATTR_THREAD, loggingEvent.ThreadName);

			if (loggingEvent.Domain != null && loggingEvent.Domain.Length > 0)
			{
				writer.WriteAttributeString(ATTR_DOMAIN, loggingEvent.Domain);
			}
			if (loggingEvent.Identity != null && loggingEvent.Identity.Length > 0)
			{
				writer.WriteAttributeString(ATTR_IDENTITY, loggingEvent.Identity);
			}
			if (loggingEvent.UserName != null && loggingEvent.UserName.Length > 0)
			{
				writer.WriteAttributeString(ATTR_USERNAME, loggingEvent.UserName);
			}
    
			// Append the message text
			writer.WriteStartElement(m_elmMessage);
			if (!this.Base64EncodeMessage)
			{
				Transform.WriteEscapedXmlString(writer, loggingEvent.RenderedMessage, this.InvalidCharReplacement);
			}
			else
			{
				byte[] messageBytes = Encoding.UTF8.GetBytes(loggingEvent.RenderedMessage);
				string base64Message = Convert.ToBase64String(messageBytes, 0, messageBytes.Length);
				Transform.WriteEscapedXmlString(writer, base64Message,this.InvalidCharReplacement);
			}
			writer.WriteEndElement();

			PropertiesDictionary properties = loggingEvent.GetProperties();

			// Append the properties text
			if (properties.Count > 0)
			{
				writer.WriteStartElement(m_elmProperties);
				foreach(System.Collections.DictionaryEntry entry in properties)
				{
					writer.WriteStartElement(m_elmData);
					writer.WriteAttributeString(ATTR_NAME, Transform.MaskXmlInvalidCharacters((string)entry.Key,this.InvalidCharReplacement));

					// Use an ObjectRenderer to convert the object to a string
					string valueStr =null;
					if (!this.Base64EncodeProperties)
					{
						valueStr = Transform.MaskXmlInvalidCharacters(loggingEvent.Repository.RendererMap.FindAndRender(entry.Value),this.InvalidCharReplacement);
					}
					else
					{
						byte[] propertyValueBytes = Encoding.UTF8.GetBytes(loggingEvent.Repository.RendererMap.FindAndRender(entry.Value));
						valueStr = Convert.ToBase64String(propertyValueBytes, 0, propertyValueBytes.Length);
					}
					writer.WriteAttributeString(ATTR_VALUE, valueStr);

					writer.WriteEndElement();
				}
				writer.WriteEndElement();
			}

			string exceptionStr = loggingEvent.GetExceptionString();
			if (exceptionStr != null && exceptionStr.Length > 0)
			{
				// Append the stack trace line
				writer.WriteStartElement(m_elmException);
				Transform.WriteEscapedXmlString(writer, exceptionStr,this.InvalidCharReplacement);
				writer.WriteEndElement();
			}

			if (LocationInfo)
			{ 
				LocationInfo locationInfo = loggingEvent.LocationInformation;

				writer.WriteStartElement(m_elmLocation);
				writer.WriteAttributeString(ATTR_CLASS, locationInfo.ClassName);
				writer.WriteAttributeString(ATTR_METHOD, locationInfo.MethodName);
				writer.WriteAttributeString(ATTR_FILE, locationInfo.FileName);
				writer.WriteAttributeString(ATTR_LINE, locationInfo.LineNumber);
				writer.WriteEndElement();
			}

			writer.WriteEndElement();
		}

		#endregion Override implementation of XMLLayoutBase

		#region Private Instance Fields
  
		/// <summary>
		/// The prefix to use for all generated element names
		/// </summary>
		private string m_prefix = PREFIX;

		private string m_elmEvent = ELM_EVENT;
		private string m_elmMessage = ELM_MESSAGE;
		private string m_elmData = ELM_DATA;
		private string m_elmProperties = ELM_PROPERTIES;
		private string m_elmException = ELM_EXCEPTION;
		private string m_elmLocation = ELM_LOCATION;

		private bool m_base64Message=false;
		private bool m_base64Properties=false;

		#endregion Private Instance Fields

		#region Private Static Fields

		private const string PREFIX = "log4net";

		private const string ELM_EVENT = "event";
		private const string ELM_MESSAGE = "message";
		private const string ELM_PROPERTIES = "properties";
		private const string ELM_GLOBAL_PROPERTIES = "global-properties";
		private const string ELM_DATA = "data";
		private const string ELM_EXCEPTION = "exception";
		private const string ELM_LOCATION = "locationInfo";

		private const string ATTR_LOGGER = "logger";
		private const string ATTR_TIMESTAMP = "timestamp";
		private const string ATTR_LEVEL = "level";
		private const string ATTR_THREAD = "thread";
		private const string ATTR_DOMAIN = "domain";
		private const string ATTR_IDENTITY = "identity";
		private const string ATTR_USERNAME = "username";
		private const string ATTR_CLASS = "class";
		private const string ATTR_METHOD = "method";
		private const string ATTR_FILE = "file";
		private const string ATTR_LINE = "line";
		private const string ATTR_NAME = "name";
		private const string ATTR_VALUE = "value";


		#endregion Private Static Fields
	}
}

