File: System\Xml\Xsl\Runtime\XmlAttributeCache.cs
Web Access
Project: src\src\libraries\System.Private.Xml\src\System.Private.Xml.csproj (System.Private.Xml)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
 
namespace System.Xml.Xsl.Runtime
{
    /// <summary>
    /// This writer supports only writer methods which write attributes.  Attributes are stored in a
    /// data structure until StartElementContent() is called, at which time the attributes are flushed
    /// to the wrapped writer.  In the case of duplicate attributes, the last attribute's value is used.
    /// </summary>
    internal sealed class XmlAttributeCache : XmlRawWriter, IRemovableWriter
    {
        private XmlRawWriter? _wrapped;
        private OnRemoveWriter? _onRemove;        // Event handler that is called when cached attributes are flushed to wrapped writer
        private AttrNameVal[]? _arrAttrs;         // List of cached attribute names and value parts
        private int _numEntries;                 // Number of attributes in the cache
        private int _idxLastName;                // The entry containing the name of the last attribute to be cached
        private int _hashCodeUnion;              // Set of hash bits that can quickly guarantee a name is not a duplicate
 
        /// <summary>
        /// Initialize the cache.  Use this method instead of a constructor in order to reuse the cache.
        /// </summary>
        public void Init(XmlRawWriter wrapped)
        {
            SetWrappedWriter(wrapped);
 
            // Clear attribute list
            _numEntries = 0;
            _idxLastName = 0;
            _hashCodeUnion = 0;
        }
 
        /// <summary>
        /// Return the number of cached attributes.
        /// </summary>
        public int Count
        {
            get { return _numEntries; }
        }
 
 
        //-----------------------------------------------
        // IRemovableWriter interface
        //-----------------------------------------------
 
        /// <summary>
        /// This writer will raise this event once cached attributes have been flushed in order to signal that the cache
        /// no longer needs to be part of the pipeline.
        /// </summary>
        public OnRemoveWriter? OnRemoveWriterEvent
        {
            get { return _onRemove; }
            set { _onRemove = value; }
        }
 
        /// <summary>
        /// The wrapped writer will callback on this method if it wishes to remove itself from the pipeline.
        /// </summary>
        private void SetWrappedWriter(XmlRawWriter? writer)
        {
            // If new writer might remove itself from pipeline, have it callback on this method when its ready to go
            IRemovableWriter? removable = writer as IRemovableWriter;
            if (removable != null)
                removable.OnRemoveWriterEvent = SetWrappedWriter;
 
            _wrapped = writer;
        }
 
 
        //-----------------------------------------------
        // XmlWriter interface
        //-----------------------------------------------
 
        /// <summary>
        /// Add an attribute to the cache.  If an attribute if the same name already exists, replace it.
        /// </summary>
        public override void WriteStartAttribute(string? prefix, string localName, string? ns)
        {
            int hashCode;
            int idx = 0;
            Debug.Assert(!string.IsNullOrEmpty(localName) && prefix != null && ns != null);
 
            // Compute hashcode based on first letter of the localName
            hashCode = (1 << ((int)localName[0] & 31));
 
            // If the hashcode is not in the union, then name will not be found by a scan
            if ((_hashCodeUnion & hashCode) != 0)
            {
                // The name may or may not be present, so scan for it
                Debug.Assert(_numEntries != 0);
 
                do
                {
                    if (_arrAttrs![idx].IsDuplicate(localName, ns, hashCode))
                        break;
 
                    // Next attribute name
                    idx = _arrAttrs[idx].NextNameIndex;
                }
                while (idx != 0);
            }
            else
            {
                // Insert hashcode into union
                _hashCodeUnion |= hashCode;
            }
 
            // Insert new attribute; link attribute names together in a list
            EnsureAttributeCache();
            if (_numEntries != 0)
                _arrAttrs![_idxLastName].NextNameIndex = _numEntries;
            _idxLastName = _numEntries++;
            _arrAttrs![_idxLastName].Init(prefix, localName, ns, hashCode);
        }
 
        /// <summary>
        /// No-op.
        /// </summary>
        public override void WriteEndAttribute()
        {
        }
 
        /// <summary>
        /// Pass through namespaces to underlying writer.  If any attributes have been cached, flush them.
        /// </summary>
        internal override void WriteNamespaceDeclaration(string prefix, string ns)
        {
            FlushAttributes();
            _wrapped!.WriteNamespaceDeclaration(prefix, ns);
        }
 
        /// <summary>
        /// Add a block of text to the cache.  This text block makes up some or all of the untyped string
        /// value of the current attribute.
        /// </summary>
        public override void WriteString(string? text)
        {
            Debug.Assert(text != null);
            Debug.Assert(_arrAttrs != null && _numEntries != 0);
            EnsureAttributeCache();
            _arrAttrs[_numEntries++].Init(text);
        }
 
        /// <summary>
        /// All other WriteValue methods are implemented by XmlWriter to delegate to WriteValue(object), so
        /// only this method needs to be implemented.
        /// </summary>
        public override void WriteValue(object value)
        {
            Debug.Assert(value is XmlAtomicValue, "value should always be an XmlAtomicValue, as XmlAttributeCache is only used by XmlQueryOutput");
            Debug.Assert(_arrAttrs != null && _numEntries != 0);
            EnsureAttributeCache();
            _arrAttrs[_numEntries++].Init((XmlAtomicValue)value);
        }
 
        /// <summary>
        /// Send cached, non-overridden attributes to the specified writer.  Calling this method has
        /// the side effect of clearing the attribute cache.
        /// </summary>
        internal override void StartElementContent()
        {
            FlushAttributes();
 
            // Call StartElementContent on wrapped writer
            _wrapped!.StartElementContent();
        }
 
        public override void WriteStartElement(string? prefix, string localName, string? ns)
        {
            Debug.Fail("Should never be called on XmlAttributeCache.");
        }
        internal override void WriteEndElement(string? prefix, string localName, string? ns)
        {
            Debug.Fail("Should never be called on XmlAttributeCache.");
        }
        public override void WriteComment(string? text)
        {
            Debug.Fail("Should never be called on XmlAttributeCache.");
        }
        public override void WriteProcessingInstruction(string name, string? text)
        {
            Debug.Fail("Should never be called on XmlAttributeCache.");
        }
        public override void WriteEntityRef(string name)
        {
            Debug.Fail("Should never be called on XmlAttributeCache.");
        }
 
        public override void WriteValue(string? value)
        {
            Debug.Fail("Should never be called on XmlAttributeCache.");
        }
 
        /// <summary>
        /// Forward call to wrapped writer.
        /// </summary>
        public override void Close()
        {
            _wrapped!.Close();
        }
 
        /// <summary>
        /// Forward call to wrapped writer.
        /// </summary>
        public override void Flush()
        {
            _wrapped!.Flush();
        }
 
 
        //-----------------------------------------------
        // Helper methods
        //-----------------------------------------------
 
        private void FlushAttributes()
        {
            int idx = 0, idxNext;
            string? localName;
 
            while (idx != _numEntries)
            {
                // Get index of next attribute's name (0 if this is the last attribute)
                idxNext = _arrAttrs![idx].NextNameIndex;
                if (idxNext == 0)
                    idxNext = _numEntries;
 
                // If localName is null, then this is a duplicate attribute that has been marked as "deleted"
                localName = _arrAttrs[idx].LocalName;
                if (localName != null)
                {
                    string prefix = _arrAttrs[idx].Prefix;
                    string ns = _arrAttrs[idx].Namespace;
 
                    _wrapped!.WriteStartAttribute(prefix, localName, ns);
 
                    // Output all of this attribute's text or typed values
                    while (++idx != idxNext)
                    {
                        string? text = _arrAttrs[idx].Text;
 
                        if (text != null)
                            _wrapped.WriteString(text);
                        else
                            _wrapped.WriteValue(_arrAttrs[idx].Value!);
                    }
 
                    _wrapped.WriteEndAttribute();
                }
                else
                {
                    // Skip over duplicate attributes
                    idx = idxNext;
                }
            }
 
            // Notify event listener that attributes have been flushed
            _onRemove?.Invoke(_wrapped!);
        }
 
        private struct AttrNameVal
        {
            private string? _localName;
            private string _prefix;
            private string _namespaceName;
            private string? _text;
            private XmlAtomicValue? _value;
            private int _hashCode;
            private int _nextNameIndex;
 
            public string? LocalName { get { return _localName; } }
            public string Prefix { get { return _prefix; } }
            public string Namespace { get { return _namespaceName; } }
            public string? Text { get { return _text; } }
            public XmlAtomicValue? Value { get { return _value; } }
            public int NextNameIndex { get { return _nextNameIndex; } set { _nextNameIndex = value; } }
 
            /// <summary>
            /// Cache an attribute's name and type.
            /// </summary>
            public void Init(string prefix, string localName, string ns, int hashCode)
            {
                _localName = localName;
                _prefix = prefix;
                _namespaceName = ns;
                _hashCode = hashCode;
                _nextNameIndex = 0;
            }
 
            /// <summary>
            /// Cache all or part of the attribute's string value.
            /// </summary>
            public void Init(string text)
            {
                _text = text;
                _value = null;
            }
 
            /// <summary>
            /// Cache all or part of the attribute's typed value.
            /// </summary>
            public void Init(XmlAtomicValue value)
            {
                _text = null;
                _value = value;
            }
 
            /// <summary>
            /// Returns true if this attribute has the specified name (and thus is a duplicate).
            /// </summary>
            public bool IsDuplicate(string localName, string ns, int hashCode)
            {
                // If attribute is not marked as deleted
                if (_localName != null)
                {
                    // And if hash codes match,
                    if (_hashCode == hashCode)
                    {
                        // And if local names match,
                        if (_localName.Equals(localName))
                        {
                            // And if namespaces match,
                            if (_namespaceName.Equals(ns))
                            {
                                // Then found duplicate attribute, so mark the attribute as deleted
                                _localName = null;
                                return true;
                            }
                        }
                    }
                }
                return false;
            }
        }
 
#if DEBUG
        private const int DefaultCacheSize = 2;
#else
        private const int DefaultCacheSize = 32;
#endif
 
        /// <summary>
        /// Ensure that attribute array has been created and is large enough for at least one
        /// additional entry.
        /// </summary>
        private void EnsureAttributeCache()
        {
            if (_arrAttrs == null)
            {
                // Create caching array
                _arrAttrs = new AttrNameVal[DefaultCacheSize];
            }
            else if (_numEntries >= _arrAttrs.Length)
            {
                // Resize caching array
                Debug.Assert(_numEntries == _arrAttrs.Length);
                AttrNameVal[] arrNew = new AttrNameVal[_numEntries * 2];
                Array.Copy(_arrAttrs, arrNew, _numEntries);
                _arrAttrs = arrNew;
            }
        }
    }
}