File: System\Xml\Dom\DocumentXmlWriter.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.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml.Schema;
 
namespace System.Xml
{
    internal enum DocumentXmlWriterType
    {
        InsertSiblingAfter,
        InsertSiblingBefore,
        PrependChild,
        AppendChild,
        AppendAttribute,
        ReplaceToFollowingSibling,
    }
 
    // Implements a XmlWriter that augments a XmlDocument.
    internal sealed class DocumentXmlWriter : XmlRawWriter, IXmlNamespaceResolver
    {
        private enum State
        {
            Error,
            Attribute,
            Prolog,
            Fragment,
            Content,
 
            Last, // always last
        }
 
        private enum Method
        {
            WriteXmlDeclaration,
            WriteStartDocument,
            WriteEndDocument,
            WriteDocType,
            WriteStartElement,
            WriteEndElement,
            WriteFullEndElement,
            WriteStartAttribute,
            WriteEndAttribute,
            WriteStartNamespaceDeclaration,
            WriteEndNamespaceDeclaration,
            WriteCData,
            WriteComment,
            WriteProcessingInstruction,
            WriteEntityRef,
            WriteWhitespace,
            WriteString,
        }
 
        private readonly DocumentXmlWriterType _type; // writer type
        private readonly XmlNode _start; // context node
        private readonly XmlDocument _document; // context document
        private XmlNamespaceManager? _namespaceManager; // context namespace manager
        private State _state; // current state
        private XmlNode? _write; // current node
        private readonly List<XmlNode> _fragment; // top level node cache
        private readonly XmlWriterSettings _settings; // wrapping writer settings
        private DocumentXPathNavigator? _navigator; // context for replace
        private XmlNode? _end; // context for replace
 
        public DocumentXmlWriter(DocumentXmlWriterType type, XmlNode start, XmlDocument document)
        {
            _type = type;
            _start = start;
            _document = document;
 
            _state = StartState();
            _fragment = new List<XmlNode>();
            _settings = new XmlWriterSettings();
            _settings.ReadOnly = false;
            _settings.CheckCharacters = false;
            _settings.CloseOutput = false;
            _settings.ConformanceLevel = (_state == State.Prolog ? ConformanceLevel.Document : ConformanceLevel.Fragment);
            _settings.ReadOnly = true;
        }
 
        public XmlNamespaceManager? NamespaceManager
        {
            set
            {
                _namespaceManager = value;
            }
        }
 
        public override XmlWriterSettings Settings
        {
            get
            {
                return _settings;
            }
        }
 
        public DocumentXPathNavigator? Navigator
        {
            set
            {
                _navigator = value;
            }
        }
 
        public XmlNode EndNode
        {
            set
            {
                _end = value;
            }
        }
 
        internal override void WriteXmlDeclaration(XmlStandalone standalone)
        {
            VerifyState(Method.WriteXmlDeclaration);
            if (standalone != XmlStandalone.Omit)
            {
                XmlNode node = _document.CreateXmlDeclaration("1.0", string.Empty, standalone == XmlStandalone.Yes ? "yes" : "no");
                AddChild(node, _write);
            }
        }
 
        internal override void WriteXmlDeclaration(string xmldecl)
        {
            VerifyState(Method.WriteXmlDeclaration);
            string? version, encoding, standalone;
            XmlLoader.ParseXmlDeclarationValue(xmldecl, out version, out encoding, out standalone);
            XmlNode node = _document.CreateXmlDeclaration(version!, encoding, standalone);
            AddChild(node, _write);
        }
 
        public override void WriteStartDocument()
        {
            VerifyState(Method.WriteStartDocument);
        }
 
        public override void WriteStartDocument(bool standalone)
        {
            VerifyState(Method.WriteStartDocument);
        }
 
        public override void WriteEndDocument()
        {
            VerifyState(Method.WriteEndDocument);
        }
 
        public override void WriteDocType(string name, string? pubid, string? sysid, string? subset)
        {
            VerifyState(Method.WriteDocType);
            XmlNode node = _document.CreateDocumentType(name, pubid, sysid, subset);
            AddChild(node, _write);
        }
 
        public override void WriteStartElement(string? prefix, string localName, string? ns)
        {
            VerifyState(Method.WriteStartElement);
            XmlNode node = _document.CreateElement(prefix, localName, ns);
            AddChild(node, _write);
            _write = node;
        }
 
        public override void WriteEndElement()
        {
            VerifyState(Method.WriteEndElement);
            if (_write == null)
            {
                throw new InvalidOperationException();
            }
 
            _write = _write.ParentNode;
        }
 
        internal override void WriteEndElement(string? prefix, string localName, string? ns)
        {
            WriteEndElement();
        }
 
        public override void WriteFullEndElement()
        {
            VerifyState(Method.WriteFullEndElement);
            XmlElement? elem = _write as XmlElement;
            if (elem == null)
            {
                throw new InvalidOperationException();
            }
 
            elem.IsEmpty = false;
            _write = elem.ParentNode;
        }
 
        internal override void WriteFullEndElement(string prefix, string localName, string ns)
        {
            WriteFullEndElement();
        }
 
        internal override void StartElementContent()
        {
            // nop
        }
 
        public override void WriteStartAttribute(string? prefix, string localName, string? ns)
        {
            VerifyState(Method.WriteStartAttribute);
            XmlAttribute attr = _document.CreateAttribute(prefix, localName, ns);
            AddAttribute(attr, _write);
            _write = attr;
        }
 
        public override void WriteEndAttribute()
        {
            VerifyState(Method.WriteEndAttribute);
            XmlAttribute? attr = _write as XmlAttribute;
            if (attr == null)
            {
                throw new InvalidOperationException();
            }
 
            if (!attr.HasChildNodes)
            {
                XmlNode node = _document.CreateTextNode(string.Empty);
                AddChild(node, attr);
            }
 
            _write = attr.OwnerElement;
        }
 
        internal override void WriteNamespaceDeclaration(string prefix, string ns)
        {
            this.WriteStartNamespaceDeclaration(prefix);
            this.WriteString(ns);
            this.WriteEndNamespaceDeclaration();
        }
 
        internal override bool SupportsNamespaceDeclarationInChunks
        {
            get
            {
                return true;
            }
        }
 
        internal override void WriteStartNamespaceDeclaration(string prefix)
        {
            VerifyState(Method.WriteStartNamespaceDeclaration);
            XmlAttribute attr;
            if (prefix.Length == 0)
            {
                attr = _document.CreateAttribute(prefix, _document.strXmlns, _document.strReservedXmlns);
            }
            else
            {
                attr = _document.CreateAttribute(_document.strXmlns, prefix, _document.strReservedXmlns);
            }
 
            AddAttribute(attr, _write);
            _write = attr;
        }
 
        internal override void WriteEndNamespaceDeclaration()
        {
            VerifyState(Method.WriteEndNamespaceDeclaration);
            XmlAttribute? attr = _write as XmlAttribute;
            if (attr == null)
            {
                throw new InvalidOperationException();
            }
 
            if (!attr.HasChildNodes)
            {
                XmlNode node = _document.CreateTextNode(string.Empty);
                AddChild(node, attr);
            }
 
            _write = attr.OwnerElement;
        }
 
        public override void WriteCData(string? text)
        {
            VerifyState(Method.WriteCData);
            XmlConvert.VerifyCharData(text, ExceptionType.ArgumentException);
            XmlNode node = _document.CreateCDataSection(text);
            AddChild(node, _write);
        }
 
        public override void WriteComment(string? text)
        {
            VerifyState(Method.WriteComment);
            XmlConvert.VerifyCharData(text, ExceptionType.ArgumentException);
            XmlNode node = _document.CreateComment(text);
            AddChild(node, _write);
        }
 
        public override void WriteProcessingInstruction(string name, string? text)
        {
            VerifyState(Method.WriteProcessingInstruction);
            XmlConvert.VerifyCharData(text, ExceptionType.ArgumentException);
            XmlNode node = _document.CreateProcessingInstruction(name, text!);
            AddChild(node, _write);
        }
 
        public override void WriteEntityRef(string name)
        {
            VerifyState(Method.WriteEntityRef);
            XmlNode node = _document.CreateEntityReference(name);
            AddChild(node, _write);
            // REVIEW: the namespace scope is incorrect(?) unless write == null
        }
 
        public override void WriteCharEntity(char ch)
        {
            WriteString(char.ToString(ch));
        }
 
        public override void WriteWhitespace(string? text)
        {
            VerifyState(Method.WriteWhitespace);
            XmlConvert.VerifyCharData(text, ExceptionType.ArgumentException);
            if (_document.PreserveWhitespace)
            {
                XmlNode node = _document.CreateWhitespace(text);
                AddChild(node, _write);
            }
        }
 
        public override void WriteString(string? text)
        {
            VerifyState(Method.WriteString);
            XmlConvert.VerifyCharData(text, ExceptionType.ArgumentException);
            XmlNode node = _document.CreateTextNode(text);
            AddChild(node, _write);
        }
 
        public override void WriteSurrogateCharEntity(char lowCh, char highCh)
        {
            WriteString(new string([highCh, lowCh]));
        }
 
        public override void WriteChars(char[] buffer, int index, int count)
        {
            WriteString(new string(buffer, index, count));
        }
 
        public override void WriteRaw(char[] buffer, int index, int count)
        {
            WriteString(new string(buffer, index, count));
        }
 
        public override void WriteRaw(string data)
        {
            WriteString(data);
        }
 
        public override void Close()
        {
            // nop
        }
 
        internal override void Close(WriteState currentState)
        {
            if (currentState == WriteState.Error)
            {
                return;
            }
            try
            {
                switch (_type)
                {
                    case DocumentXmlWriterType.InsertSiblingAfter:
                        XmlNode? parent = _start.ParentNode;
                        if (parent == null)
                        {
                            throw new InvalidOperationException(SR.Xpn_MissingParent);
                        }
 
                        for (int i = _fragment.Count - 1; i >= 0; i--)
                        {
                            parent.InsertAfter(_fragment[i], _start);
                        }
 
                        break;
                    case DocumentXmlWriterType.InsertSiblingBefore:
                        parent = _start.ParentNode;
                        if (parent == null)
                        {
                            throw new InvalidOperationException(SR.Xpn_MissingParent);
                        }
 
                        for (int i = 0; i < _fragment.Count; i++)
                        {
                            parent.InsertBefore(_fragment[i], _start);
                        }
 
                        break;
                    case DocumentXmlWriterType.PrependChild:
                        for (int i = _fragment.Count - 1; i >= 0; i--)
                        {
                            _start.PrependChild(_fragment[i]);
                        }
 
                        break;
                    case DocumentXmlWriterType.AppendChild:
                        for (int i = 0; i < _fragment.Count; i++)
                        {
                            _start.AppendChild(_fragment[i]);
                        }
 
                        break;
                    case DocumentXmlWriterType.AppendAttribute:
                        CloseWithAppendAttribute();
                        break;
                    case DocumentXmlWriterType.ReplaceToFollowingSibling:
                        if (_fragment.Count == 0)
                        {
                            throw new InvalidOperationException(SR.Xpn_NoContent);
                        }
 
                        CloseWithReplaceToFollowingSibling();
                        break;
                }
            }
            finally
            {
                _fragment.Clear();
            }
        }
 
        private void CloseWithAppendAttribute()
        {
            XmlElement? elem = _start as XmlElement;
            Debug.Assert(elem != null);
            XmlAttributeCollection attrs = elem.Attributes;
            for (int i = 0; i < _fragment.Count; i++)
            {
                XmlAttribute? attr = _fragment[i] as XmlAttribute;
                Debug.Assert(attr != null);
                int offset = attrs.FindNodeOffsetNS(attr);
                if (offset != -1
                    && ((XmlAttribute)attrs.nodes[offset]).Specified)
                {
                    throw new XmlException(SR.Xml_DupAttributeName, attr.Prefix.Length == 0 ? attr.LocalName : $"{attr.Prefix}:{attr.LocalName}");
                }
            }
            for (int i = 0; i < _fragment.Count; i++)
            {
                XmlAttribute? attr = _fragment[i] as XmlAttribute;
                Debug.Assert(attr != null);
                attrs.Append(attr);
            }
        }
 
        private void CloseWithReplaceToFollowingSibling()
        {
            XmlNode? parent = _start.ParentNode;
            if (parent == null)
            {
                throw new InvalidOperationException(SR.Xpn_MissingParent);
            }
 
            if (_start != _end)
            {
                if (!DocumentXPathNavigator.IsFollowingSibling(_start, _end))
                {
                    throw new InvalidOperationException(SR.Xpn_BadPosition);
                }
                if (_start.IsReadOnly)
                {
                    throw new InvalidOperationException(SR.Xdom_Node_Modify_ReadOnly);
                }
 
                DocumentXPathNavigator.DeleteToFollowingSibling(_start.NextSibling!, _end);
            }
 
            XmlNode fragment0 = _fragment[0];
            parent.ReplaceChild(fragment0, _start);
            for (int i = _fragment.Count - 1; i >= 1; i--)
            {
                parent.InsertAfter(_fragment[i], fragment0);
            }
 
            _navigator!.ResetPosition(fragment0);
        }
 
        public override void Flush()
        {
            // nop
        }
 
        IDictionary<string, string> IXmlNamespaceResolver.GetNamespacesInScope(XmlNamespaceScope scope)
        {
            return _namespaceManager!.GetNamespacesInScope(scope);
        }
 
        string? IXmlNamespaceResolver.LookupNamespace(string prefix)
        {
            return _namespaceManager!.LookupNamespace(prefix);
        }
 
        string? IXmlNamespaceResolver.LookupPrefix(string namespaceName)
        {
            return _namespaceManager!.LookupPrefix(namespaceName);
        }
 
        private void AddAttribute(XmlAttribute attr, XmlNode? parent)
        {
            if (parent == null)
            {
                _fragment.Add(attr);
            }
            else
            {
                XmlElement? elem = parent as XmlElement;
                if (elem == null)
                {
                    throw new InvalidOperationException();
                }
 
                elem.Attributes.Append(attr);
            }
        }
 
        private void AddChild(XmlNode node, XmlNode? parent)
        {
            if (parent == null)
            {
                _fragment.Add(node);
            }
            else
            {
                parent.AppendChild(node);
            }
        }
 
        private State StartState()
        {
            XmlNodeType nodeType = XmlNodeType.None;
 
            switch (_type)
            {
                case DocumentXmlWriterType.InsertSiblingAfter:
                case DocumentXmlWriterType.InsertSiblingBefore:
                    XmlNode? parent = _start.ParentNode;
                    if (parent != null)
                    {
                        nodeType = parent.NodeType;
                    }
 
                    if (nodeType == XmlNodeType.Document)
                    {
                        return State.Prolog;
                    }
                    else if (nodeType == XmlNodeType.DocumentFragment)
                    {
                        return State.Fragment;
                    }
 
                    break;
                case DocumentXmlWriterType.PrependChild:
                case DocumentXmlWriterType.AppendChild:
                    nodeType = _start.NodeType;
                    if (nodeType == XmlNodeType.Document)
                    {
                        return State.Prolog;
                    }
                    else if (nodeType == XmlNodeType.DocumentFragment)
                    {
                        return State.Fragment;
                    }
 
                    break;
                case DocumentXmlWriterType.AppendAttribute:
                    return State.Attribute;
                case DocumentXmlWriterType.ReplaceToFollowingSibling:
                    break;
            }
            return State.Content;
        }
 
        private static readonly State[] s_changeState = {
//          State.Error,    State.Attribute,State.Prolog,   State.Fragment, State.Content,
 
// Method.XmlDeclaration:
            State.Error,    State.Error,    State.Prolog,   State.Content,  State.Error,
// Method.StartDocument:
            State.Error,    State.Error,    State.Error,    State.Error,    State.Error,
// Method.EndDocument:
            State.Error,    State.Error,    State.Error,    State.Error,    State.Error,
// Method.DocType:
            State.Error,    State.Error,    State.Prolog,   State.Error,    State.Error,
// Method.StartElement:
            State.Error,    State.Error,    State.Content,  State.Content,  State.Content,
// Method.EndElement:
            State.Error,    State.Error,    State.Error,    State.Error,    State.Content,
// Method.FullEndElement:
            State.Error,    State.Error,    State.Error,    State.Error,    State.Content,
// Method.StartAttribute:
            State.Error,    State.Content,  State.Error,    State.Error,    State.Content,
// Method.EndAttribute:
            State.Error,    State.Error,    State.Error,    State.Error,    State.Content,
// Method.StartNamespaceDeclaration:
            State.Error,    State.Content,  State.Error,    State.Error,    State.Content,
// Method.EndNamespaceDeclaration:
            State.Error,    State.Error,    State.Error,    State.Error,    State.Content,
// Method.CData:
            State.Error,    State.Error,    State.Error,    State.Content,  State.Content,
// Method.Comment:
            State.Error,    State.Error,    State.Prolog,   State.Content,  State.Content,
// Method.ProcessingInstruction:
            State.Error,    State.Error,    State.Prolog,   State.Content,  State.Content,
// Method.EntityRef:
            State.Error,    State.Error,    State.Error,    State.Content,  State.Content,
// Method.Whitespace:
            State.Error,    State.Error,    State.Prolog,   State.Content,  State.Content,
// Method.String:
            State.Error,    State.Error,    State.Error,    State.Content,  State.Content,
        };
 
        private void VerifyState(Method method)
        {
            _state = s_changeState[(int)method * (int)State.Last + (int)_state];
            if (_state == State.Error)
            {
                throw new InvalidOperationException(SR.Xml_ClosedOrError);
            }
        }
    }
}