File: System\Xml\Linq\XNode.cs
Web Access
Project: src\src\libraries\System.Private.Xml.Linq\src\System.Private.Xml.Linq.csproj (System.Private.Xml.Linq)
// 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.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CultureInfo = System.Globalization.CultureInfo;
using StringBuilder = System.Text.StringBuilder;
using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
 
namespace System.Xml.Linq
{
    /// <summary>
    /// Represents nodes (elements, comments, document type, processing instruction,
    /// and text nodes) in the XML tree.
    /// </summary>
    /// <remarks>
    /// Nodes in the XML tree consist of objects of the following classes:
    /// <see cref="XElement"/>,
    /// <see cref="XComment"/>,
    /// <see cref="XDocument"/>,
    /// <see cref="XProcessingInstruction"/>,
    /// <see cref="XText"/>,
    /// <see cref="XDocumentType"/>
    /// Note that an <see cref="XAttribute"/> is not an <see cref="XNode"/>.
    /// </remarks>
    public abstract class XNode : XObject
    {
        private static XNodeDocumentOrderComparer? s_documentOrderComparer;
        private static XNodeEqualityComparer? s_equalityComparer;
 
        internal XNode? next;
 
        internal XNode() { }
 
        /// <summary>
        /// Gets the next sibling node of this node.
        /// </summary>
        /// <remarks>
        /// If this property does not have a parent, or if there is no next node,
        /// then this property returns null.
        /// </remarks>
        public XNode? NextNode
        {
            get
            {
                return parent == null || this == parent.content ? null : next;
            }
        }
 
        /// <summary>
        /// Gets the previous sibling node of this node.
        /// </summary>
        /// <remarks>
        /// If this property does not have a parent, or if there is no previous node,
        /// then this property returns null.
        /// </remarks>
        public XNode? PreviousNode
        {
            get
            {
                if (parent == null) return null;
                Debug.Assert(parent.content != null);
                XNode n = ((XNode)parent.content!).next!;
                XNode? p = null;
                while (n != this)
                {
                    p = n;
                    n = n.next!;
                }
                return p;
            }
        }
 
        /// <summary>
        /// Gets a comparer that can compare the relative position of two nodes.
        /// </summary>
        public static XNodeDocumentOrderComparer DocumentOrderComparer => s_documentOrderComparer ??= new XNodeDocumentOrderComparer();
 
        /// <summary>
        /// Gets a comparer that can compare two nodes for value equality.
        /// </summary>
        public static XNodeEqualityComparer EqualityComparer => s_equalityComparer ??= new XNodeEqualityComparer();
 
        /// <overloads>
        /// Adds the specified content immediately after this node. The
        /// content can be simple content, a collection of
        /// content objects, a parameter list of content objects,
        /// or null.
        /// </overloads>
        /// <summary>
        /// Adds the specified content immediately after this node.
        /// </summary>
        /// <param name="content">
        /// A content object containing simple content or a collection of content objects
        /// to be added after this node.
        /// </param>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the parent is null.
        /// </exception>
        /// <remarks>
        /// See XContainer.Add(object content) for details about the content that can be added
        /// using this method.
        /// </remarks>
        public void AddAfterSelf(object? content)
        {
            if (parent == null) throw new InvalidOperationException(SR.InvalidOperation_MissingParent);
            new Inserter(parent, this).Add(content);
        }
 
        /// <summary>
        /// Adds the specified content immediately after this node.
        /// </summary>
        /// <param name="content">
        /// A parameter list of content objects.
        /// </param>
        /// <remarks>
        /// See XContainer.Add(object content) for details about the content that can be added
        /// using this method.
        /// </remarks>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the parent is null.
        /// </exception>
        public void AddAfterSelf(params object?[] content)
        {
            AddAfterSelf((object)content);
        }
 
        /// <overloads>
        /// Adds the specified content immediately before this node. The
        /// content can be simple content, a collection of
        /// content objects, a parameter list of content objects,
        /// or null.
        /// </overloads>
        /// <summary>
        /// Adds the specified content immediately before this node.
        /// </summary>
        /// <param name="content">
        /// A content object containing simple content or a collection of content objects
        /// to be added after this node.
        /// </param>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the parent is null.
        /// </exception>
        /// <remarks>
        /// See XContainer.Add(object content) for details about the content that can be added
        /// using this method.
        /// </remarks>
        public void AddBeforeSelf(object? content)
        {
            if (parent == null) throw new InvalidOperationException(SR.InvalidOperation_MissingParent);
            XNode? p = (XNode)parent.content!;
            while (p.next != this) p = p.next!;
            if (p == parent.content) p = null;
            new Inserter(parent, p).Add(content);
        }
 
        /// <summary>
        /// Adds the specified content immediately before this node.
        /// </summary>
        /// <param name="content">
        /// A parameter list of content objects.
        /// </param>
        /// <remarks>
        /// See XContainer.Add(object content) for details about the content that can be added
        /// using this method.
        /// </remarks>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the parent is null.
        /// </exception>
        public void AddBeforeSelf(params object?[] content)
        {
            AddBeforeSelf((object)content);
        }
 
        /// <overloads>
        /// Returns an collection of the ancestor elements for this node.
        /// Optionally an node name can be specified to filter for a specific ancestor element.
        /// </overloads>
        /// <summary>
        /// Returns a collection of the ancestor elements of this node.
        /// </summary>
        /// <returns>
        /// The ancestor elements of this node.
        /// </returns>
        /// <remarks>
        /// This method will not return itself in the results.
        /// </remarks>
        public IEnumerable<XElement> Ancestors()
        {
            return GetAncestors(null, false);
        }
 
        /// <summary>
        /// Returns a collection of the ancestor elements of this node with the specified name.
        /// </summary>
        /// <param name="name">
        /// The name of the ancestor elements to find.
        /// </param>
        /// <returns>
        /// A collection of the ancestor elements of this node with the specified name.
        /// </returns>
        /// <remarks>
        /// This method will not return itself in the results.
        /// </remarks>
        public IEnumerable<XElement> Ancestors(XName? name)
        {
            return name != null ? GetAncestors(name, false) : XElement.EmptySequence;
        }
 
        /// <summary>
        /// Compares two nodes to determine their relative XML document order.
        /// </summary>
        /// <param name="n1">First node to compare.</param>
        /// <param name="n2">Second node to compare.</param>
        /// <returns>
        /// 0 if the nodes are equal; -1 if n1 is before n2; 1 if n1 is after n2.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the two nodes do not share a common ancestor.
        /// </exception>
        public static int CompareDocumentOrder(XNode? n1, XNode? n2)
        {
            if (n1 == n2) return 0;
            if (n1 == null) return -1;
            if (n2 == null) return 1;
            if (n1.parent != n2.parent)
            {
                int height = 0;
                XNode p1 = n1;
                while (p1.parent != null)
                {
                    p1 = p1.parent;
                    height++;
                }
                XNode p2 = n2;
                while (p2.parent != null)
                {
                    p2 = p2.parent;
                    height--;
                }
                if (p1 != p2) throw new InvalidOperationException(SR.InvalidOperation_MissingAncestor);
                if (height < 0)
                {
                    do
                    {
                        n2 = n2.parent!;
                        height++;
                    } while (height != 0);
                    if (n1 == n2) return -1;
                }
                else if (height > 0)
                {
                    do
                    {
                        n1 = n1.parent!;
                        height--;
                    } while (height != 0);
                    if (n1 == n2) return 1;
                }
                while (n1.parent != n2.parent)
                {
                    n1 = n1.parent!;
                    n2 = n2.parent!;
                }
            }
            else if (n1.parent == null)
            {
                throw new InvalidOperationException(SR.InvalidOperation_MissingAncestor);
            }
            XNode n = (XNode)n1.parent!.content!;
            while (true)
            {
                n = n.next!;
                if (n == n1) return -1;
                if (n == n2) return 1;
            }
        }
 
        /// <summary>
        /// Creates an <see cref="XmlReader"/> for the node.
        /// </summary>
        /// <returns>An <see cref="XmlReader"/> that can be used to read the node and its descendants.</returns>
        public XmlReader CreateReader()
        {
            return new XNodeReader(this, null);
        }
 
        /// <summary>
        /// Creates an <see cref="XmlReader"/> for the node.
        /// </summary>
        /// <param name="readerOptions">
        /// Options to be used for the returned reader. These override the default usage of annotations from the tree.
        /// </param>
        /// <returns>An <see cref="XmlReader"/> that can be used to read the node and its descendants.</returns>
        public XmlReader CreateReader(ReaderOptions readerOptions)
        {
            return new XNodeReader(this, null, readerOptions);
        }
 
        /// <summary>
        /// Returns a collection of the sibling nodes after this node, in document order.
        /// </summary>
        /// <remarks>
        /// This method only includes sibling nodes in the returned collection.
        /// </remarks>
        /// <returns>The nodes after this node.</returns>
        public IEnumerable<XNode> NodesAfterSelf()
        {
            XNode n = this;
            while (n.parent != null && n != n.parent.content)
            {
                n = n.next!;
                yield return n;
            }
        }
 
        /// <summary>
        /// Returns a collection of the sibling nodes before this node, in document order.
        /// </summary>
        /// <remarks>
        /// This method only includes sibling nodes in the returned collection.
        /// </remarks>
        /// <returns>The nodes after this node.</returns>
        public IEnumerable<XNode> NodesBeforeSelf()
        {
            if (parent != null)
            {
                XNode n = (XNode)parent.content!;
                do
                {
                    n = n.next!;
                    if (n == this) break;
                    yield return n;
                } while (parent != null && parent == n.parent);
            }
        }
 
        /// <summary>
        /// Returns a collection of the sibling element nodes after this node, in document order.
        /// </summary>
        /// <remarks>
        /// This method only includes sibling element nodes in the returned collection.
        /// </remarks>
        /// <returns>The element nodes after this node.</returns>
        public IEnumerable<XElement> ElementsAfterSelf()
        {
            return GetElementsAfterSelf(null);
        }
 
        /// <summary>
        /// Returns a collection of the sibling element nodes with the specified name
        /// after this node, in document order.
        /// </summary>
        /// <remarks>
        /// This method only includes sibling element nodes in the returned collection.
        /// </remarks>
        /// <returns>The element nodes after this node with the specified name.</returns>
        /// <param name="name">The name of elements to enumerate.</param>
        public IEnumerable<XElement> ElementsAfterSelf(XName? name)
        {
            return name != null ? GetElementsAfterSelf(name) : XElement.EmptySequence;
        }
 
        /// <summary>
        /// Returns a collection of the sibling element nodes before this node, in document order.
        /// </summary>
        /// <remarks>
        /// This method only includes sibling element nodes in the returned collection.
        /// </remarks>
        /// <returns>The element nodes before this node.</returns>
        public IEnumerable<XElement> ElementsBeforeSelf()
        {
            return GetElementsBeforeSelf(null);
        }
 
        /// <summary>
        /// Returns a collection of the sibling element nodes with the specified name
        /// before this node, in document order.
        /// </summary>
        /// <remarks>
        /// This method only includes sibling element nodes in the returned collection.
        /// </remarks>
        /// <returns>The element nodes before this node with the specified name.</returns>
        /// <param name="name">The name of elements to enumerate.</param>
        public IEnumerable<XElement> ElementsBeforeSelf(XName? name)
        {
            return name != null ? GetElementsBeforeSelf(name) : XElement.EmptySequence;
        }
 
        /// <summary>
        /// Determines if the current node appears after a specified node
        /// in terms of document order.
        /// </summary>
        /// <param name="node">The node to compare for document order.</param>
        /// <returns>True if this node appears after the specified node; false if not.</returns>
        public bool IsAfter(XNode? node)
        {
            return CompareDocumentOrder(this, node) > 0;
        }
 
        /// <summary>
        /// Determines if the current node appears before a specified node
        /// in terms of document order.
        /// </summary>
        /// <param name="node">The node to compare for document order.</param>
        /// <returns>True if this node appears before the specified node; false if not.</returns>
        public bool IsBefore(XNode? node)
        {
            return CompareDocumentOrder(this, node) < 0;
        }
 
        /// <summary>
        /// Creates an <see cref="XNode"/> from an <see cref="XmlReader"/>.
        /// The runtime type of the node is determined by the node type
        /// (<see cref="XObject.NodeType"/>) of the first node encountered
        /// in the reader.
        /// </summary>
        /// <param name="reader">An <see cref="XmlReader"/> positioned at the node to read into this <see cref="XNode"/>.</param>
        /// <returns>An <see cref="XNode"/> that contains the nodes read from the reader.</returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the <see cref="XmlReader"/> is not positioned on a recognized node type.
        /// </exception>
        public static XNode ReadFrom(XmlReader reader)
        {
            ArgumentNullException.ThrowIfNull(reader);
 
            if (reader.ReadState != ReadState.Interactive) throw new InvalidOperationException(SR.InvalidOperation_ExpectedInteractive);
            switch (reader.NodeType)
            {
                case XmlNodeType.Text:
                case XmlNodeType.SignificantWhitespace:
                case XmlNodeType.Whitespace:
                    return new XText(reader);
                case XmlNodeType.CDATA:
                    return new XCData(reader);
                case XmlNodeType.Comment:
                    return new XComment(reader);
                case XmlNodeType.DocumentType:
                    return new XDocumentType(reader);
                case XmlNodeType.Element:
                    return new XElement(reader);
                case XmlNodeType.ProcessingInstruction:
                    return new XProcessingInstruction(reader);
                default:
                    throw new InvalidOperationException(SR.Format(SR.InvalidOperation_UnexpectedNodeType, reader.NodeType));
            }
        }
 
        /// <summary>
        /// Creates an <see cref="XNode"/> from an <see cref="XmlReader"/>.
        /// The runtime type of the node is determined by the node type
        /// (<see cref="XObject.NodeType"/>) of the first node encountered
        /// in the reader.
        /// </summary>
        /// <param name="reader">An <see cref="XmlReader"/> positioned at the node to read into this <see cref="XNode"/>.</param>
        /// <param name="cancellationToken">A cancellation token.</param>
        /// <returns>An <see cref="XNode"/> that contains the nodes read from the reader.</returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the <see cref="XmlReader"/> is not positioned on a recognized node type.
        /// </exception>
        public static Task<XNode> ReadFromAsync(XmlReader reader, CancellationToken cancellationToken)
        {
            ArgumentNullException.ThrowIfNull(reader);
 
            if (cancellationToken.IsCancellationRequested)
                return Task.FromCanceled<XNode>(cancellationToken);
            return ReadFromAsyncInternal(reader, cancellationToken);
        }
 
        private static async Task<XNode> ReadFromAsyncInternal(XmlReader reader, CancellationToken cancellationToken)
        {
            if (reader.ReadState != ReadState.Interactive) throw new InvalidOperationException(SR.InvalidOperation_ExpectedInteractive);
 
            XNode ret;
 
            switch (reader.NodeType)
            {
                case XmlNodeType.Text:
                case XmlNodeType.SignificantWhitespace:
                case XmlNodeType.Whitespace:
                    ret = new XText(reader.Value);
                    break;
                case XmlNodeType.CDATA:
                    ret = new XCData(reader.Value);
                    break;
                case XmlNodeType.Comment:
                    ret = new XComment(reader.Value);
                    break;
                case XmlNodeType.DocumentType:
                    var name = reader.Name;
                    var publicId = reader.GetAttribute("PUBLIC");
                    var systemId = reader.GetAttribute("SYSTEM");
                    var internalSubset = reader.Value;
 
                    ret = new XDocumentType(name, publicId, systemId, internalSubset);
                    break;
                case XmlNodeType.Element:
                    return await XElement.CreateAsync(reader, cancellationToken).ConfigureAwait(false);
                case XmlNodeType.ProcessingInstruction:
                    var target = reader.Name;
                    var data = reader.Value;
 
                    ret = new XProcessingInstruction(target, data);
                    break;
                default:
                    throw new InvalidOperationException(SR.Format(SR.InvalidOperation_UnexpectedNodeType, reader.NodeType));
            }
 
            cancellationToken.ThrowIfCancellationRequested();
            await reader.ReadAsync().ConfigureAwait(false);
 
            return ret;
        }
 
        /// <summary>
        /// Removes this XNode from the underlying XML tree.
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the parent is null.
        /// </exception>
        public void Remove()
        {
            if (parent == null) throw new InvalidOperationException(SR.InvalidOperation_MissingParent);
            parent.RemoveNode(this);
        }
 
        /// <overloads>
        /// Replaces this node with the specified content. The
        /// content can be simple content, a collection of
        /// content objects, a parameter list of content objects,
        /// or null.
        /// </overloads>
        /// <summary>
        /// Replaces the content of this <see cref="XNode"/>.
        /// </summary>
        /// <param name="content">Content that replaces this node.</param>
        public void ReplaceWith(object? content)
        {
            if (parent == null) throw new InvalidOperationException(SR.InvalidOperation_MissingParent);
            XContainer c = parent;
            XNode? p = (XNode)parent.content!;
            while (p.next != this) p = p.next!;
            if (p == parent.content) p = null;
            parent.RemoveNode(this);
            if (p != null && p.parent != c) throw new InvalidOperationException(SR.InvalidOperation_ExternalCode);
            new Inserter(c, p).Add(content);
        }
 
        /// <summary>
        /// Replaces this node with the specified content.
        /// </summary>
        /// <param name="content">Content that replaces this node.</param>
        public void ReplaceWith(params object?[] content)
        {
            ReplaceWith((object)content);
        }
 
        /// <summary>
        /// Provides the formatted XML text representation.
        /// You can use the SaveOptions as an annotation on this node or its ancestors, then this method will use those options.
        /// </summary>
        /// <returns>A formatted XML string.</returns>
        public override string ToString()
        {
            return GetXmlString(GetSaveOptionsFromAnnotations());
        }
 
        /// <summary>
        /// Provides the XML text representation.
        /// </summary>
        /// <param name="options">
        /// If SaveOptions.DisableFormatting is enabled the output is not indented.
        /// If SaveOptions.OmitDuplicateNamespaces is enabled duplicate namespace declarations will be removed.
        /// </param>
        /// <returns>An XML string.</returns>
        public string ToString(SaveOptions options)
        {
            return GetXmlString(options);
        }
 
        /// <summary>
        /// Compares the values of two nodes, including the values of all descendant nodes.
        /// </summary>
        /// <param name="n1">The first node to compare.</param>
        /// <param name="n2">The second node to compare.</param>
        /// <returns>true if the nodes are equal, false otherwise.</returns>
        /// <remarks>
        /// A null node is equal to another null node but unequal to a non-null
        /// node. Two <see cref="XNode"/> objects of different types are never equal. Two
        /// <see cref="XText"/> nodes are equal if they contain the same text. Two
        /// <see cref="XElement"/> nodes are equal if they have the same tag name, the same
        /// set of attributes with the same values, and, ignoring comments and processing
        /// instructions, contain two equal length sequences of equal content nodes.
        /// Two <see cref="XDocument"/>s are equal if their root nodes are equal. Two
        /// <see cref="XComment"/> nodes are equal if they contain the same comment text.
        /// Two <see cref="XProcessingInstruction"/> nodes are equal if they have the same
        /// target and data. Two <see cref="XDocumentType"/> nodes are equal if the have the
        /// same name, public id, system id, and internal subset.</remarks>
        public static bool DeepEquals(XNode? n1, XNode? n2)
        {
            if (n1 == n2) return true;
            if (n1 == null || n2 == null) return false;
            return n1.DeepEquals(n2);
        }
 
        /// <summary>
        /// Write the current node to an <see cref="XmlWriter"/>.
        /// </summary>
        /// <param name="writer">The <see cref="XmlWriter"/> to write the current node into.</param>
        public abstract void WriteTo(XmlWriter writer);
 
        /// <summary>
        /// Write the current node to an <see cref="XmlWriter"/>.
        /// </summary>
        /// <param name="writer">The <see cref="XmlWriter"/> to write the current node into.</param>
        /// <param name="cancellationToken">A cancellation token.</param>
        public abstract Task WriteToAsync(XmlWriter writer, CancellationToken cancellationToken);
 
        internal virtual void AppendText(StringBuilder sb)
        {
        }
 
        internal abstract XNode CloneNode();
 
        internal abstract bool DeepEquals(XNode node);
 
        internal IEnumerable<XElement> GetAncestors(XName? name, bool self)
        {
            XElement? e = (self ? this : parent) as XElement;
            while (e != null)
            {
                if (name == null || e.name == name) yield return e;
                e = e.parent as XElement;
            }
        }
 
        private IEnumerable<XElement> GetElementsAfterSelf(XName? name)
        {
            XNode n = this;
            while (n.parent != null && n != n.parent.content)
            {
                n = n.next!;
                XElement? e = n as XElement;
                if (e != null && (name == null || e.name == name)) yield return e;
            }
        }
 
        private IEnumerable<XElement> GetElementsBeforeSelf(XName? name)
        {
            if (parent != null)
            {
                XNode n = (XNode)parent.content!;
                do
                {
                    n = n.next!;
                    if (n == this) break;
                    XElement? e = n as XElement;
                    if (e != null && (name == null || e.name == name)) yield return e;
                } while (parent != null && parent == n.parent);
            }
        }
 
        internal abstract int GetDeepHashCode();
 
        // The settings simulate a non-validating processor with the external
        // entity resolution disabled. The processing of the internal subset is
        // enabled by default. In order to prevent DoS attacks, the expanded
        // size of the internal subset is limited to 10 million characters.
        internal static XmlReaderSettings GetXmlReaderSettings(LoadOptions o)
        {
            XmlReaderSettings rs = new XmlReaderSettings();
            if ((o & LoadOptions.PreserveWhitespace) == 0) rs.IgnoreWhitespace = true;
 
            // DtdProcessing.Parse; Parse is not defined in the public contract
            rs.DtdProcessing = (DtdProcessing)2;
            rs.MaxCharactersFromEntities = (long)1e7;
            // rs.XmlResolver = null;
            return rs;
        }
 
        internal static XmlWriterSettings GetXmlWriterSettings(SaveOptions o)
        {
            XmlWriterSettings ws = new XmlWriterSettings();
            if ((o & SaveOptions.DisableFormatting) == 0) ws.Indent = true;
            if ((o & SaveOptions.OmitDuplicateNamespaces) != 0) ws.NamespaceHandling |= NamespaceHandling.OmitDuplicates;
            return ws;
        }
 
        private string GetXmlString(SaveOptions o)
        {
            using (StringWriter sw = new StringWriter(CultureInfo.InvariantCulture))
            {
                XmlWriterSettings ws = new XmlWriterSettings();
                ws.OmitXmlDeclaration = true;
                if ((o & SaveOptions.DisableFormatting) == 0) ws.Indent = true;
                if ((o & SaveOptions.OmitDuplicateNamespaces) != 0) ws.NamespaceHandling |= NamespaceHandling.OmitDuplicates;
                if (this is XText) ws.ConformanceLevel = ConformanceLevel.Fragment;
                using (XmlWriter w = XmlWriter.Create(sw, ws))
                {
                    XDocument? n = this as XDocument;
                    if (n != null)
                    {
                        n.WriteContentTo(w);
                    }
                    else
                    {
                        WriteTo(w);
                    }
                }
                return sw.ToString();
            }
        }
    }
}