File: System\Diagnostics\ListenerElementsCollection.cs
Web Access
Project: src\src\libraries\System.Configuration.ConfigurationManager\src\System.Configuration.ConfigurationManager.csproj (System.Configuration.ConfigurationManager)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Xml;
 
namespace System.Diagnostics
{
    [ConfigurationCollection(typeof(ListenerElement))]
    internal class ListenerElementsCollection : ConfigurationElementCollection
    {
        public new ListenerElement this[string name] => (ListenerElement)BaseGet(name);
 
        public override ConfigurationElementCollectionType CollectionType => ConfigurationElementCollectionType.AddRemoveClearMap;
 
        protected override ConfigurationElement CreateNewElement() => new ListenerElement(true);
 
        protected override object GetElementKey(ConfigurationElement element) => ((ListenerElement)element).Name;
 
        public IEnumerable<TraceListener> GetRuntimeObject()
        {
            List<TraceListener> listeners = new();
 
            foreach (ListenerElement element in this)
            {
                // At some point, we need to pull out adding/removing the 'default' DefaultTraceListener
                // code from here in favor of adding/not-adding after we load the config (in TraceSource
                // and in static Trace).
 
                listeners.Add(element.GetRuntimeObject());
            }
 
            return listeners;
        }
 
        protected internal override void InitializeDefault() => InitializeDefaultInternal();
 
        internal void InitializeDefaultInternal()
        {
            ListenerElement defaultListener = new ListenerElement(false);
            defaultListener.Name = "Default";
            defaultListener.TypeName = typeof(DefaultTraceListener).FullName;
            defaultListener._isAddedByDefault = true;
 
            BaseAdd(defaultListener);
        }
 
        protected override void BaseAdd(ConfigurationElement element)
        {
            ListenerElement listenerElement = element as ListenerElement;
 
            Debug.Assert((listenerElement != null), "adding elements other than ListenerElement to ListenerElementsCollection?");
 
            if (listenerElement.Name.Equals("Default") && listenerElement.TypeName.Equals(typeof(DefaultTraceListener).FullName))
                BaseAdd(listenerElement, false);
            else
                BaseAdd(listenerElement, ThrowOnDuplicate);
        }
    }
 
    // This is the collection used by the sharedListener section.  It is only slightly different from ListenerElementsCollection.
    // The differences are that it does not allow remove and clear, and that the ListenerElements it creates do not allow
    // references.
    [ConfigurationCollection(typeof(ListenerElement),
        AddItemName = "add",
        CollectionType = ConfigurationElementCollectionType.BasicMap)]
    internal sealed class SharedListenerElementsCollection : ListenerElementsCollection
    {
        public override ConfigurationElementCollectionType CollectionType => ConfigurationElementCollectionType.BasicMap;
        protected override ConfigurationElement CreateNewElement() => new ListenerElement(false);
        protected override string ElementName => "add";
    }
 
    internal sealed class ListenerElement : TypedElement
    {
        private static readonly ConfigurationProperty s_propFilter = new("filter", typeof(FilterElement), null, ConfigurationPropertyOptions.None);
        private static readonly ConfigurationProperty s_propName = new("name", typeof(string), null, ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey);
        private static readonly ConfigurationProperty s_propOutputOpts = new("traceOutputOptions", typeof(TraceOptions), TraceOptions.None, ConfigurationPropertyOptions.None);
 
        private readonly ConfigurationProperty _propListenerTypeName;
        private readonly bool _allowReferences;
        private StringDictionary _attributes;
        internal bool _isAddedByDefault;
 
        private static readonly ConditionalWeakTable<TraceListener, string> s_initData = new();
 
        public ListenerElement(bool allowReferences) : base(typeof(TraceListener))
        {
            _allowReferences = allowReferences;
 
            ConfigurationPropertyOptions flags = ConfigurationPropertyOptions.None;
            if (!_allowReferences)
                flags |= ConfigurationPropertyOptions.IsRequired;
 
            _propListenerTypeName = new ConfigurationProperty("type", typeof(string), null, flags);
 
            _properties.Remove("type");
            _properties.Add(_propListenerTypeName);
            _properties.Add(s_propFilter);
            _properties.Add(s_propName);
            _properties.Add(s_propOutputOpts);
        }
 
        public StringDictionary Attributes => _attributes ?? new StringDictionary();
 
        [ConfigurationProperty("filter")]
        public FilterElement Filter => (FilterElement)this[s_propFilter];
 
        [ConfigurationProperty("name", IsRequired = true, IsKey = true)]
        public string Name
        {
            get
            {
                return (string)this[s_propName];
            }
            set
            {
                this[s_propName] = value;
            }
        }
 
        [ConfigurationProperty("traceOutputOptions", DefaultValue = (TraceOptions)TraceOptions.None)]
        public TraceOptions TraceOutputOptions
        {
            get
            {
                return (TraceOptions)this[s_propOutputOpts];
            }
            // This is useful when the OM becomes public. In the meantime, this can be utilized via reflection.
            set
            {
                this[s_propOutputOpts] = value;
            }
 
        }
 
        [ConfigurationProperty("type")]
        public override string TypeName
        {
            get
            {
                return (string)this[_propListenerTypeName];
            }
            set
            {
                this[_propListenerTypeName] = value;
            }
        }
 
        public override bool Equals(object compareTo)
        {
            if (Name.Equals("Default") && TypeName.Equals(typeof(DefaultTraceListener).FullName))
            {
                // This is a workaround to treat all DefaultTraceListener named 'Default' the same.
                // This is needed for the Config.Save to work properly as otherwise config base layers
                // above us would run into duplicate 'Default' listener element and perceive it as
                // error.
                ListenerElement compareToElem = compareTo as ListenerElement;
                return (compareToElem != null) && compareToElem.Name.Equals("Default")
                        && compareToElem.TypeName.Equals(typeof(DefaultTraceListener).FullName);
            }
 
            return base.Equals(compareTo);
        }
 
        public override int GetHashCode() => base.GetHashCode();
 
        public TraceListener GetRuntimeObject()
        {
            if (_runtimeObject != null)
                return (TraceListener)_runtimeObject;
 
            try
            {
                string className = TypeName;
                if (string.IsNullOrEmpty(className))
                {
                    // Look it up in SharedListeners.
                    Debug.Assert(_allowReferences, "_allowReferences must be true if type name is null");
 
                    if (_attributes != null || ElementInformation.Properties[s_propFilter.Name].ValueOrigin == PropertyValueOrigin.SetHere || TraceOutputOptions != TraceOptions.None || !string.IsNullOrEmpty(InitData))
                    {
                        throw new ConfigurationErrorsException(SR.Format(SR.Reference_listener_cant_have_properties, Name));
                    }
 
                    if (DiagnosticsConfiguration.SharedListeners == null)
                    {
                        throw new ConfigurationErrorsException(SR.Format(SR.Reference_to_nonexistent_listener, Name));
                    }
 
                    ListenerElement sharedListener = DiagnosticsConfiguration.SharedListeners[Name];
                    if (sharedListener == null)
                    {
                        throw new ConfigurationErrorsException(SR.Format(SR.Reference_to_nonexistent_listener, Name));
                    }
 
                    _runtimeObject = sharedListener.GetRuntimeObject();
                    return (TraceListener)_runtimeObject;
                }
                else
                {
                    // Create a new one.
                    TraceListener newListener = (TraceListener)BaseGetRuntimeObject();
                    s_initData.AddOrUpdate(newListener, InitData);
                    newListener.Name = Name;
                    TraceUtils.CopyStringDictionary(Attributes, newListener.Attributes);
                    newListener.TraceOutputOptions = TraceOutputOptions;
 
                    if (Filter != null && !string.IsNullOrEmpty(Filter.TypeName))
                    {
                        newListener.Filter = Filter.GetRuntimeObject();
                    }
 
                    _runtimeObject = newListener;
                    return newListener;
                }
            }
            catch (ArgumentException e)
            {
                throw new ConfigurationErrorsException(SR.Format(SR.Could_not_create_listener, Name), e);
            }
        }
 
        // Our optional attributes implementation is little convoluted as there is
        // no such first class mechanism from the config system. We basically cache
        // any "unrecognized" attribute here and serialize it out later.
        protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
        {
            Attributes.Add(name, value);
            return true;
        }
 
        // We need to serialize optional attributes here, a better place would have
        // been inside SerializeElement but the base class implementation from
        // ConfigurationElement doesn't take into account for derived class doing
        // extended serialization, it basically writes out child element that
        // forces the element closing syntax, so any attribute serialization needs
        // to happen before normal element serialization from ConfigurationElement.
        // This means we would write out custom attributes ahead of normal ones.
        // The other alternative would be to re-implement the entire routine here
        // which is an overkill and a maintenance issue.
        protected override void PreSerialize(XmlWriter writer)
        {
            if (_attributes != null)
            {
                IDictionaryEnumerator e = (IDictionaryEnumerator)_attributes.GetEnumerator();
                while (e.MoveNext())
                {
                    string xmlValue = (string)e.Value;
                    string xmlName = (string)e.Key;
 
                    if ((xmlValue != null) && (writer != null))
                    {
                        writer.WriteAttributeString(xmlName, xmlValue);
                    }
                }
            }
        }
 
        // Account for optional attributes from custom listeners.
        protected internal override bool SerializeElement(XmlWriter writer, bool serializeCollectionKey)
        {
            bool DataToWrite = base.SerializeElement(writer, serializeCollectionKey);
            DataToWrite = DataToWrite || ((_attributes != null) && (_attributes.Count > 0));
            return DataToWrite;
        }
 
        protected internal override void Unmerge(ConfigurationElement sourceElement,
                                                 ConfigurationElement parentElement,
                                                 ConfigurationSaveMode saveMode)
        {
            base.Unmerge(sourceElement, parentElement, saveMode);
 
            // Unmerge the optional attributes cache as well
            ListenerElement le = sourceElement as ListenerElement;
            if ((le != null) && (le._attributes != null))
            {
                _attributes = le._attributes;
            }
        }
 
        internal void ResetProperties()
        {
            // Blow away any UnrecognizedAttributes that we have deserialized earlier.
            if (_attributes != null)
            {
                _attributes.Clear();
                _properties.Clear();
                _properties.Add(_propListenerTypeName);
                _properties.Add(s_propFilter);
                _properties.Add(s_propName);
                _properties.Add(s_propOutputOpts);
            }
        }
 
        internal TraceListener RefreshRuntimeObject(TraceListener listener)
        {
            _runtimeObject = null;
            try
            {
                string className = TypeName;
                if (string.IsNullOrEmpty(className))
                {
                    // Look it up in SharedListeners and ask the sharedListener to refresh.
                    Debug.Assert(_allowReferences, "_allowReferences must be true if type name is null");
 
                    if (_attributes != null || ElementInformation.Properties[s_propFilter.Name].ValueOrigin == PropertyValueOrigin.SetHere || TraceOutputOptions != TraceOptions.None || !string.IsNullOrEmpty(InitData))
                    {
                        throw new ConfigurationErrorsException(SR.Format(SR.Reference_listener_cant_have_properties, Name));
                    }
 
                    if (DiagnosticsConfiguration.SharedListeners == null)
                    {
                        throw new ConfigurationErrorsException(SR.Format(SR.Reference_to_nonexistent_listener, Name));
                    }
 
                    ListenerElement sharedListener = DiagnosticsConfiguration.SharedListeners[Name];
                    if (sharedListener == null)
                    {
                        throw new ConfigurationErrorsException(SR.Format(SR.Reference_to_nonexistent_listener, Name));
                    }
 
                    _runtimeObject = sharedListener.RefreshRuntimeObject(listener);
                    return (TraceListener)_runtimeObject;
                }
                else
                {
                    // We're the element with the type and initializeData info.  First see if those two are the same as they were.
                    // If not, create a whole new object, otherwise, just update the other properties.
                    if (Type.GetType(className) != listener.GetType() || InitDataChanged(listener))
                    {
                        // Type or initdata changed.
                        return GetRuntimeObject();
                    }
                    else
                    {
                        TraceUtils.CopyStringDictionary(Attributes, listener.Attributes);
                        listener.TraceOutputOptions = TraceOutputOptions;
 
                        if (listener.Filter != null)
                        {
                            if (ElementInformation.Properties[s_propFilter.Name].ValueOrigin == PropertyValueOrigin.SetHere ||
                                ElementInformation.Properties[s_propFilter.Name].ValueOrigin == PropertyValueOrigin.Inherited)
                            {
                                listener.Filter = Filter.RefreshRuntimeObject(listener.Filter);
                            }
                            else
                            {
                                listener.Filter = null;
                            }
                        }
 
                        _runtimeObject = listener;
                        return listener;
                    }
                }
            }
            catch (ArgumentException e)
            {
                throw new ConfigurationErrorsException(SR.Format(SR.Could_not_create_listener, Name), e);
            }
        }
 
        private bool InitDataChanged(TraceListener listener) => !s_initData.TryGetValue(listener, out string previousInitData)
            || InitData != previousInitData;
    }
}