src/log4net/Util/PatternParser.cs (193 lines of code) (raw):

#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.Collections; using System.Globalization; using log4net.Core; using log4net.Layout; namespace log4net.Util; /// <summary> /// Most of the work of the <see cref="PatternLayout"/> class /// is delegated to the PatternParser class. /// </summary> /// <remarks> /// <para> /// The <c>PatternParser</c> processes a pattern string and /// returns a chain of <see cref="PatternConverter"/> objects. /// </para> /// </remarks> /// <author>Nicko Cadell</author> /// <author>Gert Driesen</author> public sealed class PatternParser { /// <summary> /// Constructor /// </summary> /// <param name="pattern">The pattern to parse.</param> /// <remarks> /// <para> /// Initializes a new instance of the <see cref="PatternParser" /> class /// with the specified pattern string. /// </para> /// </remarks> public PatternParser(string pattern) => this._pattern = pattern; /// <summary> /// Parses the pattern into a chain of pattern converters. /// </summary> /// <returns>The head of a chain of pattern converters.</returns> public PatternConverter? Parse() { string[] converterNamesCache = BuildCache(); ParseInternal(_pattern, converterNamesCache); return _head; } /// <summary> /// Gets the converter registry used by this parser. /// </summary> public Hashtable PatternConverters { get; } = []; /// <summary> /// Build the unified cache of converters from the static and instance maps /// </summary> /// <returns>the list of all the converter names</returns> private string[] BuildCache() { string[] converterNamesCache = new string[PatternConverters.Keys.Count]; PatternConverters.Keys.CopyTo(converterNamesCache, 0); // sort array so that longer strings come first Array.Sort(converterNamesCache, 0, converterNamesCache.Length, StringLengthComparer.Instance); return converterNamesCache; } /// <summary> /// Sort strings by length /// </summary> /// <remarks> /// <para> /// <see cref="IComparer" /> that orders strings by string length. /// The longest strings are placed first /// </para> /// </remarks> private sealed class StringLengthComparer : IComparer { public static readonly StringLengthComparer Instance = new(); private StringLengthComparer() { } public int Compare(object? x, object? y) { string? s1 = x as string; string? s2 = y as string; if (s1 is null && s2 is null) { return 0; } if (s1 is null) { return 1; } if (s2 is null) { return -1; } return s2.Length.CompareTo(s1.Length); } } /// <summary> /// Internal method to parse the specified pattern to find specified matches /// </summary> /// <param name="pattern">the pattern to parse</param> /// <param name="matches">the converter names to match in the pattern</param> /// <remarks> /// <para> /// The matches param must be sorted such that longer strings come before shorter ones. /// </para> /// </remarks> private void ParseInternal(string pattern, string[] matches) { int offset = 0; while (offset < pattern.Length) { int i = pattern.IndexOf(EscapeChar, offset); if (i < 0 || i == pattern.Length - 1) { ProcessLiteral(pattern.Substring(offset)); offset = pattern.Length; } else { if (pattern[i + 1] == EscapeChar) { // Escaped ProcessLiteral(pattern.Substring(offset, i - offset + 1)); offset = i + 2; } else { ProcessLiteral(pattern.Substring(offset, i - offset)); offset = i + 1; var formattingInfo = new FormattingInfo(); // Process formatting options // Look for the align flag if (offset < pattern.Length) { if (pattern[offset] == '-') { // Seen align flag formattingInfo.LeftAlign = true; offset++; } } // Look for the minimum length while (offset < pattern.Length && char.IsDigit(pattern[offset])) { // Seen digit if (formattingInfo.Min < 0) { formattingInfo.Min = 0; } formattingInfo.Min = (formattingInfo.Min * 10) + int.Parse(pattern[offset].ToString(), NumberFormatInfo.InvariantInfo); offset++; } // Look for the separator between min and max if (offset < pattern.Length) { if (pattern[offset] == '.') { // Seen separator offset++; } } // Look for the maximum length while (offset < pattern.Length && char.IsDigit(pattern[offset])) { // Seen digit if (formattingInfo.Max == int.MaxValue) { formattingInfo.Max = 0; } formattingInfo.Max = (formattingInfo.Max * 10) + int.Parse(pattern[offset].ToString(), NumberFormatInfo.InvariantInfo); offset++; } int remainingStringLength = pattern.Length - offset; // Look for pattern for (int m = 0; m < matches.Length; m++) { string key = matches[m]; if (key.Length <= remainingStringLength) { if (string.Compare(pattern, offset, key, 0, key.Length, StringComparison.Ordinal) == 0) { // Found match offset += matches[m].Length; string? option = null; // Look for option if (offset < pattern.Length) { if (pattern[offset] == '{') { // Seen option start offset++; int optEnd = pattern.IndexOf('}', offset); if (optEnd < 0) { // error } else { option = pattern.Substring(offset, optEnd - offset); offset = optEnd + 1; } } } ProcessConverter(matches[m], option, formattingInfo); break; } } } } } } } /// <summary> /// Process a parsed literal /// </summary> /// <param name="text">the literal text</param> private void ProcessLiteral(string text) { if (text.Length > 0) { // Convert into a pattern ProcessConverter("literal", text, new FormattingInfo()); } } /// <summary> /// Process a parsed converter pattern /// </summary> /// <param name="converterName">the name of the converter</param> /// <param name="option">the optional option for the converter</param> /// <param name="formattingInfo">the formatting info for the converter</param> private void ProcessConverter(string converterName, string? option, FormattingInfo formattingInfo) { LogLog.Debug(_declaringType, $"Converter [{converterName}] Option [{option}] Format [min={formattingInfo.Min},max={formattingInfo.Max},leftAlign={formattingInfo.LeftAlign}]"); // Lookup the converter type if (PatternConverters[converterName] is not ConverterInfo converterInfo) { LogLog.Error(_declaringType, $"Unknown converter name [{converterName}] in conversion pattern."); } else { // Create the pattern converter PatternConverter pc; try { pc = Activator.CreateInstance(converterInfo.Type.EnsureNotNull()).EnsureIs<PatternConverter>(); } catch (Exception e) when (!e.IsFatal()) { LogLog.Error(_declaringType, $"Failed to create instance of Type [{converterInfo.Type?.FullName}] using default constructor. Exception: {e}"); return; } // formattingInfo variable is an instance variable, occasionally reset // and used over and over again pc.FormattingInfo = formattingInfo; pc.Option = option; pc.Properties = converterInfo.Properties; if (pc is IOptionHandler optionHandler) { optionHandler.ActivateOptions(); } AddConverter(pc); } } /// <summary> /// Resets the internal state of the parser and adds the specified pattern converter /// to the chain. /// </summary> /// <param name="pc">The pattern converter to add.</param> private void AddConverter(PatternConverter pc) { // Add the pattern converter to the list. if (_head is null) { _head = _tail = pc; } else { // Set the next converter on the tail // Update the tail reference // note that a converter may combine the 'next' into itself // and therefore the tail would not change! _tail = _tail!.SetNext(pc); } } private const char EscapeChar = '%'; /// <summary> /// The first pattern converter in the chain /// </summary> private PatternConverter? _head; /// <summary> /// the last pattern converter in the chain /// </summary> private PatternConverter? _tail; /// <summary> /// The pattern /// </summary> private readonly string _pattern; /// <summary> /// The fully qualified type of the PatternParser class. /// </summary> /// <remarks> /// Used by the internal logger to record the Type of the /// log message. /// </remarks> private static readonly Type _declaringType = typeof(PatternParser); }