niall 2005/08/24 06:26:38 Modified: src/Layout XMLLayout.cs XMLLayoutBase.cs XmlLayoutSchemaLog4j.cs src/Util Transform.cs tests/src log4net.Tests.csproj Added: tests/src/Layout XmlLayoutTest.cs Log: Fixes for LOG4NET-22 and LOG4NET-44 with associated tests. Characters that cannot be expressed in XML are now masked with a user specifiable charater. The message and property values may be base64 encoded if this is undesirable. The name of the properties node has been fixed to properties rather than global-properties. Revision Changes Path 1.11 +66 -7 logging-log4net/src/Layout/XMLLayout.cs Index: XMLLayout.cs =================================================================== RCS file: /home/cvs/logging-log4net/src/Layout/XMLLayout.cs,v retrieving revision 1.10 retrieving revision 1.11 diff -u -r1.10 -r1.11 --- XMLLayout.cs 17 Jan 2005 20:18:45 -0000 1.10 +++ XMLLayout.cs 24 Aug 2005 13:26:37 -0000 1.11 @@ -121,6 +121,46 @@ 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="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="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 @@ -154,7 +194,6 @@ m_elmEvent = m_prefix + ":" + ELM_EVENT; m_elmMessage = m_prefix + ":" + ELM_MESSAGE; m_elmProperties = m_prefix + ":" + ELM_PROPERTIES; - m_elmGlobalProperties = m_prefix + ":" + ELM_GLOBAL_PROPERTIES; m_elmData = m_prefix + ":" + ELM_DATA; m_elmException = m_prefix + ":" + ELM_EXCEPTION; m_elmLocation = m_prefix + ":" + ELM_LOCATION; @@ -199,7 +238,16 @@ // Append the message text writer.WriteStartElement(m_elmMessage); - Transform.WriteEscapedXmlString(writer, loggingEvent.RenderedMessage); + if (!this.Base64EncodeMessage) + { + string message=loggingEvent.RenderedMessage; + + Transform.WriteEscapedXmlString(writer, loggingEvent.RenderedMessage,this.InvalidCharReplacement); + } + else + { + Transform.WriteEscapedXmlString(writer, Convert.ToBase64String(Encoding.UTF8.GetBytes(loggingEvent.RenderedMessage)),this.InvalidCharReplacement); + } writer.WriteEndElement(); PropertiesDictionary properties = loggingEvent.GetProperties(); @@ -207,14 +255,22 @@ // Append the properties text if (properties.Count > 0) { - writer.WriteStartElement(m_elmGlobalProperties); + writer.WriteStartElement(m_elmProperties); foreach(System.Collections.DictionaryEntry entry in properties) { writer.WriteStartElement(m_elmData); - writer.WriteAttributeString(ATTR_NAME, (string)entry.Key); + writer.WriteAttributeString(ATTR_NAME, Transform.MaskXMLInvalidCharacters((string)entry.Key,this.InvalidCharReplacement)); // Use an ObjectRenderer to convert the object to a string - string valueStr = loggingEvent.Repository.RendererMap.FindAndRender(entry.Value); + string valueStr =null; + if (!this.Base64EncodeProperties) + { + valueStr=Transform.MaskXMLInvalidCharacters(loggingEvent.Repository.RendererMap.FindAndRender(entry.Value),this.InvalidCharReplacement); + } + else + { + valueStr=Convert.ToBase64String(Encoding.UTF8.GetBytes(loggingEvent.Repository.RendererMap.FindAndRender(entry.Value))); + } writer.WriteAttributeString(ATTR_VALUE, valueStr); writer.WriteEndElement(); @@ -227,7 +283,7 @@ { // Append the stack trace line writer.WriteStartElement(m_elmException); - Transform.WriteEscapedXmlString(writer, exceptionStr); + Transform.WriteEscapedXmlString(writer, exceptionStr,this.InvalidCharReplacement); writer.WriteEndElement(); } @@ -259,10 +315,12 @@ private string m_elmMessage = ELM_MESSAGE; private string m_elmData = ELM_DATA; private string m_elmProperties = ELM_PROPERTIES; - private string m_elmGlobalProperties = ELM_GLOBAL_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 @@ -291,6 +349,7 @@ private const string ATTR_NAME = "name"; private const string ATTR_VALUE = "value"; + #endregion Private Static Fields } } 1.7 +23 -1 logging-log4net/src/Layout/XMLLayoutBase.cs Index: XMLLayoutBase.cs =================================================================== RCS file: /home/cvs/logging-log4net/src/Layout/XMLLayoutBase.cs,v retrieving revision 1.6 retrieving revision 1.7 diff -u -r1.6 -r1.7 --- XMLLayoutBase.cs 17 Jan 2005 20:18:45 -0000 1.6 +++ XMLLayoutBase.cs 24 Aug 2005 13:26:37 -0000 1.7 @@ -110,7 +110,24 @@ get { return m_locationInfo; } set { m_locationInfo = value; } } - + /// <summary> + /// The string to replace characters that can not be expressed in XML with. + /// <remarks> + /// <para> + /// Not all characters may be expressed in XML. This property contains the + /// string to replace those that can not with. This defaults to a ?. Set it + /// to the empty string to simply remove offending characters. For more + /// details on the allowed character ranges see http://www.w3.org/TR/REC-xml/#charsets + /// Character replacement will occur in the log message, the property names + /// and the property values. + /// </para> + /// </remarks> + /// </summary> + public string InvalidCharReplacement + { + get {return m_invalidCharReplacement;} + set {m_invalidCharReplacement=value;} + } #endregion #region Implementation of IOptionHandler @@ -231,6 +248,11 @@ /// </summary> private readonly ProtectCloseTextWriter m_protectCloseTextWriter = new ProtectCloseTextWriter(null); + /// <summary> + /// The string to replace invalid chars with + /// </summary> + private string m_invalidCharReplacement="?"; + #endregion Private Instance Fields } } 1.12 +3 -3 logging-log4net/src/Layout/XmlLayoutSchemaLog4j.cs Index: XmlLayoutSchemaLog4j.cs =================================================================== RCS file: /home/cvs/logging-log4net/src/Layout/XmlLayoutSchemaLog4j.cs,v retrieving revision 1.11 retrieving revision 1.12 diff -u -r1.11 -r1.12 --- XmlLayoutSchemaLog4j.cs 7 Feb 2005 23:14:37 -0000 1.11 +++ XmlLayoutSchemaLog4j.cs 24 Aug 2005 13:26:37 -0000 1.12 @@ -187,7 +187,7 @@ // Append the message text writer.WriteStartElement("log4j:message"); - Transform.WriteEscapedXmlString(writer, loggingEvent.RenderedMessage); + Transform.WriteEscapedXmlString(writer, loggingEvent.RenderedMessage,this.InvalidCharReplacement); writer.WriteEndElement(); object ndcObj = loggingEvent.LookupProperty("NDC"); @@ -199,7 +199,7 @@ { // Append the NDC text writer.WriteStartElement("log4j:NDC"); - Transform.WriteEscapedXmlString(writer, valueStr); + Transform.WriteEscapedXmlString(writer, valueStr,this.InvalidCharReplacement); writer.WriteEndElement(); } } @@ -228,7 +228,7 @@ { // Append the stack trace line writer.WriteStartElement("log4j:throwable"); - Transform.WriteEscapedXmlString(writer, exceptionStr); + Transform.WriteEscapedXmlString(writer, exceptionStr,this.InvalidCharReplacement); writer.WriteEndElement(); } 1.7 +11 -2 logging-log4net/src/Util/Transform.cs Index: Transform.cs =================================================================== RCS file: /home/cvs/logging-log4net/src/Util/Transform.cs,v retrieving revision 1.6 retrieving revision 1.7 diff -u -r1.6 -r1.7 --- Transform.cs 7 Feb 2005 04:05:50 -0000 1.6 +++ Transform.cs 24 Aug 2005 13:26:37 -0000 1.7 @@ -19,6 +19,7 @@ using System; using System.Text; using System.Xml; +using System.Text.RegularExpressions; namespace log4net.Util { @@ -56,15 +57,17 @@ /// Write a string to an <see cref="XmlWriter"/> /// </summary> /// <param name="writer">the writer to write to</param> - /// <param name="stringData">the string to write</param> + /// <param name="textData">the string to write</param> + /// <param name="invalidCharReplacement">The string to replace non XML compliant chars with</param> /// <remarks> /// <para> /// The test is escaped either using XML escape entities /// or using CDATA sections. /// </para> /// </remarks> - public static void WriteEscapedXmlString(XmlWriter writer, string stringData) + public static void WriteEscapedXmlString(XmlWriter writer, string textData, string invalidCharReplacement) { + string stringData=MaskXMLInvalidCharacters(textData,invalidCharReplacement); // Write either escaped text or CDATA sections int weightCData = 12 * (1 + CountSubstrings(stringData, CDATA_END)); @@ -113,6 +116,11 @@ } } + public static string MaskXMLInvalidCharacters(string textData,string mask) + { + return INVALIDCHARS.Replace(textData,mask); + } + #endregion Public Static Methods #region Private Helper Methods @@ -166,6 +174,7 @@ private const string CDATA_END = "]]>"; private const string CDATA_UNESCAPABLE_TOKEN = "]]"; + private static Regex INVALIDCHARS=new Regex(@"[^\x09\x0A\x0D\x20-\xFF\u00FF-\u07FF\uE000-\uFFFD]",RegexOptions.Compiled); #endregion Private Static Fields } } 1.12 +5 -0 logging-log4net/tests/src/log4net.Tests.csproj Index: log4net.Tests.csproj =================================================================== RCS file: /home/cvs/logging-log4net/tests/src/log4net.Tests.csproj,v retrieving revision 1.11 retrieving revision 1.12 diff -u -r1.11 -r1.12 --- log4net.Tests.csproj 20 Jun 2005 19:11:53 -0000 1.11 +++ log4net.Tests.csproj 24 Aug 2005 13:26:37 -0000 1.12 @@ -158,6 +158,11 @@ BuildAction = "Compile" /> <File + RelPath = "Layout\XmlLayoutTest.cs" + SubType = "Code" + BuildAction = "Compile" + /> + <File RelPath = "Util\CyclicBufferTest.cs" SubType = "Code" BuildAction = "Compile" 1.1 logging-log4net/tests/src/Layout/XmlLayoutTest.cs Index: XmlLayoutTest.cs =================================================================== #region Copyright & License // // Copyright 2001-2005 The Apache Software Foundation // // Licensed 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.IO; using System.Diagnostics; using System.Globalization; using log4net.Config; using log4net.Util; using log4net.Layout; using log4net.Core; using log4net.Appender; using log4net.Repository; using log4net.Tests.Appender; using NUnit.Framework; namespace log4net.Tests.Layout { [TestFixture] public class XmlLayoutTest { /// <summary> /// Build a basic <see cref="LoggingEventData"/> object with some default values. /// </summary> /// <returns>A useful LoggingEventData object</returns> private LoggingEventData createBaseEvent() { LoggingEventData ed=new LoggingEventData(); ed.Domain="Tests"; ed.ExceptionString=""; ed.Identity="TestRunner"; ed.Level=Level.Info; ed.LocationInfo=new LocationInfo(this.GetType()); ed.LoggerName="TestLogger"; ed.Message="Test message"; ed.ThreadName="TestThread"; ed.TimeStamp=new DateTime(2005,8,24,12,0,0); ed.UserName="TestRunner"; ed.Properties=new PropertiesDictionary(); return ed; } [Test] public void TestBasicEventLogging() { TextWriter writer=new StringWriter(); XmlLayout layout=new XmlLayout(); LoggingEventData evt=createBaseEvent(); layout.Format(writer,new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>Test message</message></event>\r\n", writer.ToString() ); } [Test] public void TestIllegalCharacterMasking() { TextWriter writer=new StringWriter(); XmlLayout layout=new XmlLayout(); LoggingEventData evt=createBaseEvent(); evt.Message="This is a masked char->\uFFFF"; layout.Format(writer,new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>This is a masked char->?</message></event>\r\n", writer.ToString() ); } [Test] public void TestCDATAEscaping1() { TextWriter writer=new StringWriter(); XmlLayout layout=new XmlLayout(); LoggingEventData evt=createBaseEvent(); //The &'s trigger the use of a cdata block evt.Message="&&&&&&&Escape this ]]>. End here."; layout.Format(writer,new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message><![CDATA[&&&&&&&Escape this ]]>]]<![CDATA[>. End here.]]></message></event>\r\n", writer.ToString() ); } [Test] public void TestCDATAEscaping2() { TextWriter writer=new StringWriter(); XmlLayout layout=new XmlLayout(); LoggingEventData evt=createBaseEvent(); //The &'s trigger the use of a cdata block evt.Message="&&&&&&&Escape the end ]]>"; layout.Format(writer,new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message><![CDATA[&&&&&&&Escape the end ]]>]]></message></event>\r\n", writer.ToString() ); } [Test] public void TestCDATAEscaping3() { TextWriter writer=new StringWriter(); XmlLayout layout=new XmlLayout(); LoggingEventData evt=createBaseEvent(); //The &'s trigger the use of a cdata block evt.Message="]]>&&&&&&&Escape the begining"; layout.Format(writer,new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message><![CDATA[]]>]]<![CDATA[>&&&&&&&Escape the begining]]></message></event>\r\n", writer.ToString() ); } [Test] public void TestBase64EventLogging() { TextWriter writer=new StringWriter(); XmlLayout layout=new XmlLayout(); LoggingEventData evt=createBaseEvent(); layout.Base64EncodeMessage=true; layout.Format(writer,new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>VGVzdCBtZXNzYWdl</message></event>\r\n", writer.ToString() ); } [Test] public void TestPropertyEventLogging() { LoggingEventData evt=createBaseEvent(); evt.Properties["Property1"]="prop1"; XmlLayout layout=new XmlLayout(); StringAppender stringAppender = new StringAppender(); stringAppender.Layout = layout; ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString()); BasicConfigurator.Configure(rep, stringAppender); ILog log1 = LogManager.GetLogger(rep.Name, "TestThreadProperiesPattern"); log1.Logger.Log(new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>Test message</message><properties><data name=\"Property1\" value=\"prop1\" /></properties></event>\r\n", stringAppender.GetString() ); } [Test] public void TestBase64PropertyEventLogging() { LoggingEventData evt=createBaseEvent(); evt.Properties["Property1"]="prop1"; XmlLayout layout=new XmlLayout(); layout.Base64EncodeProperties=true; StringAppender stringAppender = new StringAppender(); stringAppender.Layout = layout; ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString()); BasicConfigurator.Configure(rep, stringAppender); ILog log1 = LogManager.GetLogger(rep.Name, "TestThreadProperiesPattern"); log1.Logger.Log(new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>Test message</message><properties><data name=\"Property1\" value=\"cHJvcDE=\" /></properties></event>\r\n", stringAppender.GetString() ); } [Test] public void TestPropertyCharacterEscaping() { LoggingEventData evt=createBaseEvent(); evt.Properties["Property1"]="prop1 \"quoted\""; XmlLayout layout=new XmlLayout(); StringAppender stringAppender = new StringAppender(); stringAppender.Layout = layout; ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString()); BasicConfigurator.Configure(rep, stringAppender); ILog log1 = LogManager.GetLogger(rep.Name, "TestThreadProperiesPattern"); log1.Logger.Log(new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>Test message</message><properties><data name=\"Property1\" value=\"prop1 "quoted"\" /></properties></event>\r\n", stringAppender.GetString() ); } [Test] public void TestPropertyIllegalCharacterMasking() { LoggingEventData evt=createBaseEvent(); evt.Properties["Property1"]="mask this ->\uFFFF"; XmlLayout layout=new XmlLayout(); StringAppender stringAppender = new StringAppender(); stringAppender.Layout = layout; ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString()); BasicConfigurator.Configure(rep, stringAppender); ILog log1 = LogManager.GetLogger(rep.Name, "TestThreadProperiesPattern"); log1.Logger.Log(new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>Test message</message><properties><data name=\"Property1\" value=\"mask this ->?\" /></properties></event>\r\n", stringAppender.GetString() ); } [Test] public void TestPropertyIllegalCharacterMaskingInName() { LoggingEventData evt=createBaseEvent(); evt.Properties["Property\uFFFF"]="mask this ->\uFFFF"; XmlLayout layout=new XmlLayout(); StringAppender stringAppender = new StringAppender(); stringAppender.Layout = layout; ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString()); BasicConfigurator.Configure(rep, stringAppender); ILog log1 = LogManager.GetLogger(rep.Name, "TestThreadProperiesPattern"); log1.Logger.Log(new LoggingEvent(evt)); Assertion.AssertEquals ( "<event logger=\"TestLogger\" timestamp=\"2005-08-24T12:00:00.0000000+01:00\" level=\"INFO\" thread=\"TestThread\" domain=\"Tests\" identity=\"TestRunner\" username=\"TestRunner\"><message>Test message</message><properties><data name=\"Property?\" value=\"mask this ->?\" /></properties></event>\r\n", stringAppender.GetString() ); } } }