File: System\Xml\Core\XsdCachingReader.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.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml.Schema;
using System.Xml.XPath;
 
namespace System.Xml
{
    internal sealed partial class XsdCachingReader : XmlReader, IXmlLineInfo
    {
        private enum CachingReaderState
        {
            None = 0,
            Init = 1,
            Record = 2,
            Replay = 3,
            ReaderClosed = 4,
            Error = 5,
        }
 
        private XmlReader _coreReader;
        private XmlNameTable _coreReaderNameTable;
 
        private ValidatingReaderNodeData[] _contentEvents;
        private ValidatingReaderNodeData[] _attributeEvents;
 
        private ValidatingReaderNodeData? _cachedNode;
 
        private CachingReaderState _cacheState;
        private int _contentIndex;
        private int _attributeCount;
 
        private bool _returnOriginalStringValues;
 
        private readonly CachingEventHandler _cacheHandler;
 
        //current state
        private int _currentAttrIndex;
        private int _currentContentIndex;
        private bool _readAhead;
 
        //Lineinfo
        private readonly IXmlLineInfo? _lineInfo;
 
        //ReadAttributeValue TextNode
        private ValidatingReaderNodeData? _textNode;
 
        //Constants
        private const int InitialAttributeCount = 8;
        private const int InitialContentCount = 4;
 
        //Constructor
        internal XsdCachingReader(XmlReader reader, IXmlLineInfo? lineInfo, CachingEventHandler handlerMethod)
        {
            _coreReader = reader;
            _lineInfo = lineInfo;
            _cacheHandler = handlerMethod;
            _attributeEvents = new ValidatingReaderNodeData[InitialAttributeCount];
            _contentEvents = new ValidatingReaderNodeData[InitialContentCount];
            Init();
        }
 
        [MemberNotNull(nameof(_coreReaderNameTable))]
        private void Init()
        {
            _coreReaderNameTable = _coreReader.NameTable;
            _cacheState = CachingReaderState.Init;
            _contentIndex = 0;
            _currentAttrIndex = -1;
            _currentContentIndex = -1;
            _attributeCount = 0;
            _cachedNode = null;
            _readAhead = false;
            //Initialize the cachingReader with start state
            if (_coreReader.NodeType == XmlNodeType.Element)
            {
                ValidatingReaderNodeData element = AddContent(_coreReader.NodeType);
                element.SetItemData(_coreReader.LocalName, _coreReader.Prefix, _coreReader.NamespaceURI, _coreReader.Depth);  //Only created for element node type
                element.SetLineInfo(_lineInfo);
                RecordAttributes();
            }
        }
 
        internal void Reset(XmlReader reader)
        {
            _coreReader = reader;
            Init();
        }
 
        // Settings
        public override XmlReaderSettings? Settings
        {
            get
            {
                return _coreReader.Settings;
            }
        }
 
        // Node Properties
 
        // Gets the type of the current node.
        public override XmlNodeType NodeType
        {
            get
            {
                return _cachedNode!.NodeType;
            }
        }
 
        // Gets the name of the current node, including the namespace prefix.
        public override string Name
        {
            get
            {
                return _cachedNode!.GetAtomizedNameWPrefix(_coreReaderNameTable);
            }
        }
 
        // Gets the name of the current node without the namespace prefix.
        public override string LocalName
        {
            get
            {
                return _cachedNode!.LocalName;
            }
        }
 
        // Gets the namespace URN (as defined in the W3C Namespace Specification) of the current namespace scope.
        public override string NamespaceURI
        {
            get
            {
                return _cachedNode!.Namespace;
            }
        }
 
        // Gets the namespace prefix associated with the current node.
        public override string Prefix
        {
            get
            {
                return _cachedNode!.Prefix;
            }
        }
 
        // Gets a value indicating whether the current node can have a non-empty Value.
        public override bool HasValue
        {
            get
            {
                return XmlReader.HasValueInternal(_cachedNode!.NodeType);
            }
        }
 
        // Gets the text value of the current node.
        public override string Value
        {
            get
            {
                return _returnOriginalStringValues ? _cachedNode!.OriginalStringValue! : _cachedNode!.RawValue;
            }
        }
 
        // Gets the depth of the current node in the XML element stack.
        public override int Depth
        {
            get
            {
                return _cachedNode!.Depth;
            }
        }
 
        // Gets the base URI of the current node.
        public override string BaseURI
        {
            get
            {
                return _coreReader.BaseURI;
            }
        }
 
        // Gets a value indicating whether the current node is an empty element (for example, <MyElement/>).
        public override bool IsEmptyElement
        {
            get
            {
                return false;
            }
        }
 
        // Gets a value indicating whether the current node is an attribute that was generated from the default value defined
        // in the DTD or schema.
        public override bool IsDefault
        {
            get
            {
                return false;
            }
        }
 
        // Gets the quotation mark character used to enclose the value of an attribute node.
        public override char QuoteChar
        {
            get
            {
                return _coreReader.QuoteChar;
            }
        }
 
        // Gets the current xml:space scope.
        public override XmlSpace XmlSpace
        {
            get
            {
                return _coreReader.XmlSpace;
            }
        }
 
        // Gets the current xml:lang scope.
        public override string XmlLang
        {
            get
            {
                return _coreReader.XmlLang;
            }
        }
 
        // Attribute Accessors
 
        // The number of attributes on the current node.
        public override int AttributeCount
        {
            get
            {
                return _attributeCount;
            }
        }
 
        // Gets the value of the attribute with the specified Name.
        public override string? GetAttribute(string name)
        {
            int i;
            if (!name.Contains(':'))
            {
                i = GetAttributeIndexWithoutPrefix(name);
            }
            else
            {
                i = GetAttributeIndexWithPrefix(name);
            }
 
            return (i >= 0) ? _attributeEvents[i].RawValue : null;
        }
 
        // Gets the value of the attribute with the specified LocalName and NamespaceURI.
        public override string? GetAttribute(string name, string? namespaceURI)
        {
            namespaceURI = (namespaceURI == null) ? string.Empty : _coreReaderNameTable.Get(namespaceURI);
            string? atomizedName = _coreReaderNameTable.Get(name);
            ValidatingReaderNodeData attribute;
            for (int i = 0; i < _attributeCount; i++)
            {
                attribute = _attributeEvents[i];
                if (Ref.Equal(attribute.LocalName, atomizedName) && Ref.Equal(attribute.Namespace, namespaceURI))
                {
                    return attribute.RawValue;
                }
            }
 
            return null;
        }
 
        // Gets the value of the attribute with the specified index.
        public override string GetAttribute(int i)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(i);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(i, _attributeCount);
 
            return _attributeEvents[i].RawValue;
        }
 
        // Gets the value of the attribute with the specified index.
        public override string this[int i]
        {
            get
            {
                return GetAttribute(i);
            }
        }
 
        // Gets the value of the attribute with the specified LocalName and NamespaceURI.
        public override string? this[string name, string? namespaceURI]
        {
            get
            {
                return GetAttribute(name, namespaceURI);
            }
        }
 
        // Moves to the attribute with the specified Name.
        public override bool MoveToAttribute(string name)
        {
            int i;
            if (!name.Contains(':'))
            {
                i = GetAttributeIndexWithoutPrefix(name);
            }
            else
            {
                i = GetAttributeIndexWithPrefix(name);
            }
 
            if (i >= 0)
            {
                _currentAttrIndex = i;
                _cachedNode = _attributeEvents[i];
                return true;
            }
            else
            {
                return false;
            }
        }
 
        // Moves to the attribute with the specified LocalName and NamespaceURI
        public override bool MoveToAttribute(string name, string? ns)
        {
            ns = (ns == null) ? string.Empty : _coreReaderNameTable.Get(ns);
            string? atomizedName = _coreReaderNameTable.Get(name);
            ValidatingReaderNodeData attribute;
            for (int i = 0; i < _attributeCount; i++)
            {
                attribute = _attributeEvents[i];
                if (Ref.Equal(attribute.LocalName, atomizedName) &&
                     Ref.Equal(attribute.Namespace, ns))
                {
                    _currentAttrIndex = i;
                    _cachedNode = _attributeEvents[i];
                    return true;
                }
            }
 
            return false;
        }
 
        // Moves to the attribute with the specified index.
        public override void MoveToAttribute(int i)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(i);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(i, _attributeCount);
            _currentAttrIndex = i;
            _cachedNode = _attributeEvents[i];
        }
 
        // Moves to the first attribute.
        public override bool MoveToFirstAttribute()
        {
            if (_attributeCount == 0)
            {
                return false;
            }
            _currentAttrIndex = 0;
            _cachedNode = _attributeEvents[0];
            return true;
        }
 
        // Moves to the next attribute.
        public override bool MoveToNextAttribute()
        {
            if (_currentAttrIndex + 1 < _attributeCount)
            {
                _cachedNode = _attributeEvents[++_currentAttrIndex];
                return true;
            }
            return false;
        }
 
        // Moves to the element that contains the current attribute node.
        public override bool MoveToElement()
        {
            if (_cacheState != CachingReaderState.Replay || _cachedNode!.NodeType != XmlNodeType.Attribute)
            {
                return false;
            }
 
            _currentContentIndex = 0;
            _currentAttrIndex = -1;
            Read();
            return true;
        }
 
        // Reads the next node from the stream/TextReader.
        public override bool Read()
        {
            switch (_cacheState)
            {
                case CachingReaderState.Init:
                    _cacheState = CachingReaderState.Record;
                    goto case CachingReaderState.Record;
 
                case CachingReaderState.Record:
                    ValidatingReaderNodeData? recordedNode = null;
                    if (_coreReader.Read())
                    {
                        switch (_coreReader.NodeType)
                        {
                            case XmlNodeType.Element:
                                //Dont record element within the content of a union type since the main reader will break on this and the underlying coreReader will be positioned on this node
                                _cacheState = CachingReaderState.ReaderClosed;
                                return false;
 
                            case XmlNodeType.EndElement:
                                recordedNode = AddContent(_coreReader.NodeType);
                                recordedNode.SetItemData(_coreReader.LocalName, _coreReader.Prefix, _coreReader.NamespaceURI, _coreReader.Depth);  //Only created for element node type
                                recordedNode.SetLineInfo(_lineInfo);
                                break;
 
                            case XmlNodeType.Comment:
                            case XmlNodeType.ProcessingInstruction:
                            case XmlNodeType.Text:
                            case XmlNodeType.CDATA:
                            case XmlNodeType.Whitespace:
                            case XmlNodeType.SignificantWhitespace:
                                recordedNode = AddContent(_coreReader.NodeType);
                                recordedNode.SetItemData(_coreReader.Value);
                                recordedNode.SetLineInfo(_lineInfo);
                                recordedNode.Depth = _coreReader.Depth;
                                break;
 
                            default:
                                break;
                        }
                        _cachedNode = recordedNode;
                        return true;
                    }
                    else
                    {
                        _cacheState = CachingReaderState.ReaderClosed;
                        return false;
                    }
 
                case CachingReaderState.Replay:
                    if (_currentContentIndex >= _contentIndex)
                    { //When positioned on the last cached node, switch back as the underlying coreReader is still positioned on this node
                        _cacheState = CachingReaderState.ReaderClosed;
                        _cacheHandler(this);
                        if (_coreReader.NodeType != XmlNodeType.Element || _readAhead)
                        { //Only when coreReader not positioned on Element node, read ahead, otherwise it is on the next element node already, since this was not cached
                            return _coreReader.Read();
                        }
                        return true;
                    }
                    _cachedNode = _contentEvents[_currentContentIndex];
                    if (_currentContentIndex > 0)
                    {
                        ClearAttributesInfo();
                    }
                    _currentContentIndex++;
                    return true;
 
                default:
                    return false;
            }
        }
 
        internal ValidatingReaderNodeData RecordTextNode(string textValue, string? originalStringValue, int depth, int lineNo, int linePos)
        {
            ValidatingReaderNodeData textNode = AddContent(XmlNodeType.Text);
            textNode.SetItemData(textValue, originalStringValue);
            textNode.SetLineInfo(lineNo, linePos);
            textNode.Depth = depth;
            return textNode;
        }
 
        internal void SwitchTextNodeAndEndElement(string textValue, string? originalStringValue)
        {
            Debug.Assert(_coreReader.NodeType == XmlNodeType.EndElement || (_coreReader.NodeType == XmlNodeType.Element && _coreReader.IsEmptyElement));
 
            ValidatingReaderNodeData textNode = RecordTextNode(textValue, originalStringValue, _coreReader.Depth + 1, 0, 0);
            int endElementIndex = _contentIndex - 2;
            ValidatingReaderNodeData endElementNode = _contentEvents[endElementIndex];
            Debug.Assert(endElementNode.NodeType == XmlNodeType.EndElement);
            _contentEvents[endElementIndex] = textNode;
            _contentEvents[_contentIndex - 1] = endElementNode;
        }
 
        internal void RecordEndElementNode()
        {
            ValidatingReaderNodeData recordedNode = AddContent(XmlNodeType.EndElement);
            Debug.Assert(_coreReader.NodeType == XmlNodeType.EndElement || (_coreReader.NodeType == XmlNodeType.Element && _coreReader.IsEmptyElement));
            recordedNode.SetItemData(_coreReader.LocalName, _coreReader.Prefix, _coreReader.NamespaceURI, _coreReader.Depth);
            recordedNode.SetLineInfo(_coreReader as IXmlLineInfo);
            if (_coreReader.IsEmptyElement)
            { //Simulated endElement node for <e/>, the coreReader is on cached Element node itself.
                _readAhead = true;
            }
        }
 
        internal string ReadOriginalContentAsString()
        {
            _returnOriginalStringValues = true;
            string strValue = InternalReadContentAsString();
            _returnOriginalStringValues = false;
            return strValue;
        }
 
        // Gets a value indicating whether XmlReader is positioned at the end of the stream.
        public override bool EOF
        {
            get
            {
                return _cacheState == CachingReaderState.ReaderClosed && _coreReader.EOF;
            }
        }
 
        // Closes the stream, changes the ReadState to Closed, and sets all the properties back to zero.
        public override void Close()
        {
            _coreReader.Close();
            _cacheState = CachingReaderState.ReaderClosed;
        }
 
        // Returns the read state of the stream.
        public override ReadState ReadState
        {
            get
            {
                return _coreReader.ReadState;
            }
        }
 
        // Skips to the end tag of the current element.
        public override void Skip()
        {
            //Skip on caching reader should move to the end of the subtree, past all cached events
            switch (_cachedNode!.NodeType)
            {
                case XmlNodeType.Element:
                    if (_coreReader.NodeType != XmlNodeType.EndElement && !_readAhead)
                    { //will be true for IsDefault cases where we peek only one node ahead
                        int startDepth = _coreReader.Depth - 1;
                        while (_coreReader.Read() && _coreReader.Depth > startDepth)
                            ;
                    }
                    _coreReader.Read();
                    _cacheState = CachingReaderState.ReaderClosed;
                    _cacheHandler(this);
                    break;
 
                case XmlNodeType.Attribute:
                    MoveToElement();
                    goto case XmlNodeType.Element;
 
                default:
                    Debug.Assert(_cacheState == CachingReaderState.Replay);
                    Read();
                    break;
            }
        }
 
        // Gets the XmlNameTable associated with this implementation.
        public override XmlNameTable NameTable
        {
            get
            {
                return _coreReaderNameTable;
            }
        }
 
        // Resolves a namespace prefix in the current element's scope.
        public override string? LookupNamespace(string prefix)
        {
            return _coreReader.LookupNamespace(prefix);
        }
 
        // Resolves the entity reference for nodes of NodeType EntityReference.
        public override void ResolveEntity()
        {
            throw new InvalidOperationException();
        }
 
        // Parses the attribute value into one or more Text and/or EntityReference node types.
        public override bool ReadAttributeValue()
        {
            Debug.Assert(_cacheState == CachingReaderState.Replay);
            if (_cachedNode!.NodeType != XmlNodeType.Attribute)
            {
                return false;
            }
 
            _cachedNode = CreateDummyTextNode(_cachedNode.RawValue, _cachedNode.Depth + 1);
            return true;
        }
 
        //
        // IXmlLineInfo members
        //
 
        bool IXmlLineInfo.HasLineInfo()
        {
            return true;
        }
 
        int IXmlLineInfo.LineNumber
        {
            get
            {
                return _cachedNode!.LineNumber;
            }
        }
 
        int IXmlLineInfo.LinePosition
        {
            get
            {
                return _cachedNode!.LinePosition;
            }
        }
 
        //Private methods
        internal void SetToReplayMode()
        {
            _cacheState = CachingReaderState.Replay;
            _currentContentIndex = 0;
            _currentAttrIndex = -1;
            Read(); //Position on first node recorded to begin replaying
        }
 
        internal XmlReader GetCoreReader()
        {
            return _coreReader;
        }
 
        internal IXmlLineInfo? GetLineInfo()
        {
            return _lineInfo;
        }
 
        private void ClearAttributesInfo()
        {
            _attributeCount = 0;
            _currentAttrIndex = -1;
        }
 
        private ValidatingReaderNodeData AddAttribute(int attIndex)
        {
            Debug.Assert(attIndex < _attributeEvents.Length);
            ValidatingReaderNodeData attInfo = _attributeEvents[attIndex];
            if (attInfo != null)
            {
                attInfo.Clear(XmlNodeType.Attribute);
                return attInfo;
            }
            if (attIndex >= _attributeEvents.Length - 1)
            { //reached capacity of array, Need to increase capacity to twice the initial
                ValidatingReaderNodeData[] newAttributeEvents = new ValidatingReaderNodeData[_attributeEvents.Length * 2];
                Array.Copy(_attributeEvents, newAttributeEvents, _attributeEvents.Length);
                _attributeEvents = newAttributeEvents;
            }
            return _attributeEvents[attIndex] ??= new ValidatingReaderNodeData(XmlNodeType.Attribute);
        }
 
        private ValidatingReaderNodeData AddContent(XmlNodeType nodeType)
        {
            Debug.Assert(_contentIndex <= _contentEvents.Length);
            ValidatingReaderNodeData contentInfo = _contentEvents[_contentIndex];
            if (contentInfo != null)
            {
                contentInfo.Clear(nodeType);
                _contentIndex++;
                return contentInfo;
            }
            if (_contentIndex >= _contentEvents.Length - 1)
            { //reached capacity of array, Need to increase capacity to twice the initial
                ValidatingReaderNodeData[] newContentEvents = new ValidatingReaderNodeData[_contentEvents.Length * 2];
                Array.Copy(_contentEvents, newContentEvents, _contentEvents.Length);
                _contentEvents = newContentEvents;
            }
            contentInfo = _contentEvents[_contentIndex] ??= new ValidatingReaderNodeData(nodeType);
            _contentIndex++;
            return contentInfo;
        }
 
        private void RecordAttributes()
        {
            Debug.Assert(_coreReader.NodeType == XmlNodeType.Element);
            ValidatingReaderNodeData attInfo;
            _attributeCount = _coreReader.AttributeCount;
            if (_coreReader.MoveToFirstAttribute())
            {
                int attIndex = 0;
                do
                {
                    attInfo = AddAttribute(attIndex);
                    attInfo.SetItemData(_coreReader.LocalName, _coreReader.Prefix, _coreReader.NamespaceURI, _coreReader.Depth);
                    attInfo.SetLineInfo(_lineInfo);
                    attInfo.RawValue = _coreReader.Value;
                    attIndex++;
                } while (_coreReader.MoveToNextAttribute());
                _coreReader.MoveToElement();
            }
        }
 
        private int GetAttributeIndexWithoutPrefix(string name)
        {
            string? atomizedName = _coreReaderNameTable.Get(name);
            if (atomizedName == null)
            {
                return -1;
            }
 
            ValidatingReaderNodeData attribute;
            for (int i = 0; i < _attributeCount; i++)
            {
                attribute = _attributeEvents[i];
                if (Ref.Equal(attribute.LocalName, atomizedName) && attribute.Prefix.Length == 0)
                {
                    return i;
                }
            }
            return -1;
        }
 
        private int GetAttributeIndexWithPrefix(string name)
        {
            string? atomizedName = _coreReaderNameTable.Get(name);
            if (atomizedName == null)
            {
                return -1;
            }
            ValidatingReaderNodeData attribute;
            for (int i = 0; i < _attributeCount; i++)
            {
                attribute = _attributeEvents[i];
                if (Ref.Equal(attribute.GetAtomizedNameWPrefix(_coreReaderNameTable), atomizedName))
                {
                    return i;
                }
            }
 
            return -1;
        }
 
        private ValidatingReaderNodeData CreateDummyTextNode(string attributeValue, int depth)
        {
            _textNode ??= new ValidatingReaderNodeData(XmlNodeType.Text);
 
            _textNode.Depth = depth;
            _textNode.RawValue = attributeValue;
            return _textNode;
        }
    }
}