File: System\Xml\Schema\XNodeValidator.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;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Versioning;
using System.Xml;
using System.Xml.Linq;
 
namespace System.Xml.Schema
{
    internal sealed class XNodeValidator
    {
        private readonly XmlSchemaSet schemas;
        private readonly ValidationEventHandler? validationEventHandler;
 
        private XObject? source;
        private bool addSchemaInfo;
        private XmlNamespaceManager? namespaceManager;
        private XmlSchemaValidator? validator;
 
        private HashSet<XmlSchemaInfo>? schemaInfos;
        private ArrayList? defaultAttributes;
        private readonly XName xsiTypeName;
        private readonly XName xsiNilName;
 
        public XNodeValidator(XmlSchemaSet schemas, ValidationEventHandler? validationEventHandler)
        {
            this.schemas = schemas;
            this.validationEventHandler = validationEventHandler;
 
            XNamespace xsi = XNamespace.Get("http://www.w3.org/2001/XMLSchema-instance");
            xsiTypeName = xsi.GetName("type");
            xsiNilName = xsi.GetName("nil");
        }
 
        public void Validate(XObject source, XmlSchemaObject? partialValidationType, bool addSchemaInfo)
        {
            this.source = source;
            this.addSchemaInfo = addSchemaInfo;
            XmlSchemaValidationFlags validationFlags = XmlSchemaValidationFlags.AllowXmlAttributes;
            XmlNodeType nt = source.NodeType;
            switch (nt)
            {
                case XmlNodeType.Document:
                    source = ((XDocument)source).Root!;
                    if (source == null) throw new InvalidOperationException(SR.InvalidOperation_MissingRoot);
                    validationFlags |= XmlSchemaValidationFlags.ProcessIdentityConstraints;
                    break;
                case XmlNodeType.Element:
                    break;
                case XmlNodeType.Attribute:
                    if (((XAttribute)source).IsNamespaceDeclaration) goto default;
                    if (source.Parent == null) throw new InvalidOperationException(SR.InvalidOperation_MissingParent);
                    break;
                default:
                    throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadNodeType, nt));
            }
            namespaceManager = new XmlNamespaceManager(schemas.NameTable);
            PushAncestorsAndSelf(source.Parent);
            validator = new XmlSchemaValidator(schemas.NameTable, schemas, namespaceManager, validationFlags);
            validator.ValidationEventHandler += new ValidationEventHandler(ValidationCallback);
            validator.XmlResolver = null;
            if (partialValidationType != null)
            {
                validator.Initialize(partialValidationType);
            }
            else
            {
                validator.Initialize();
            }
 
            IXmlLineInfo original = SaveLineInfo(source);
            if (nt == XmlNodeType.Attribute)
            {
                ValidateAttribute((XAttribute)source);
            }
            else
            {
                ValidateElement((XElement)source);
            }
            validator.EndValidation();
            RestoreLineInfo(original);
        }
 
        private XmlSchemaInfo GetDefaultAttributeSchemaInfo(XmlSchemaAttribute sa)
        {
            XmlSchemaInfo si = new XmlSchemaInfo();
            si.IsDefault = true;
            si.IsNil = false;
            si.SchemaAttribute = sa;
            Debug.Assert(sa.AttributeSchemaType != null);
            XmlSchemaSimpleType st = sa.AttributeSchemaType;
            si.SchemaType = st;
            Debug.Assert(st.Datatype != null);
            if (st.Datatype.Variety == XmlSchemaDatatypeVariety.Union)
            {
                string? value = GetDefaultValue(sa);
                Debug.Assert(st.Content != null);
                foreach (XmlSchemaSimpleType mt in ((XmlSchemaSimpleTypeUnion)st.Content).BaseMemberTypes!)
                {
                    object? typedValue = null;
                    try
                    {
                        Debug.Assert(mt.Datatype != null);
                        Debug.Assert(value != null);
                        typedValue = mt.Datatype.ParseValue(value, schemas.NameTable, namespaceManager);
                    }
                    catch (XmlSchemaException)
                    {
                    }
                    if (typedValue != null)
                    {
                        si.MemberType = mt;
                        break;
                    }
                }
            }
            si.Validity = XmlSchemaValidity.Valid;
            return si;
        }
 
        private string? GetDefaultValue(XmlSchemaAttribute sa)
        {
            XmlSchemaAttribute? saCopy = sa;
            XmlQualifiedName name = saCopy.RefName;
            if (!name.IsEmpty)
            {
                saCopy = schemas.GlobalAttributes[name] as XmlSchemaAttribute;
                if (saCopy == null) return null;
            }
            string? s = saCopy.FixedValue;
            if (s != null) return s;
            return saCopy.DefaultValue;
        }
 
        private string? GetDefaultValue(XmlSchemaElement se)
        {
            XmlSchemaElement? seCopy = se;
            XmlQualifiedName name = seCopy.RefName;
            if (!name.IsEmpty)
            {
                seCopy = schemas.GlobalElements[name] as XmlSchemaElement;
                if (seCopy == null) return null;
            }
            string? s = seCopy.FixedValue;
            if (s != null) return s;
            return seCopy.DefaultValue;
        }
 
        private void ReplaceSchemaInfo(XObject o, XmlSchemaInfo schemaInfo)
        {
            schemaInfos ??= new HashSet<XmlSchemaInfo>(new XmlSchemaInfoEqualityComparer());
            XmlSchemaInfo? si = o.Annotation<XmlSchemaInfo>();
            if (si != null)
            {
                schemaInfos.Add(si);
                o.RemoveAnnotations<XmlSchemaInfo>();
            }
            if (!schemaInfos.TryGetValue(schemaInfo, out si))
            {
                si = schemaInfo;
                schemaInfos.Add(si);
            }
            o.AddAnnotation(si);
        }
 
        private void PushAncestorsAndSelf(XElement? e)
        {
            while (e != null)
            {
                XAttribute? a = e.lastAttr;
                if (a != null)
                {
                    do
                    {
                        a = a.next!;
                        if (a.IsNamespaceDeclaration)
                        {
                            string localName = a.Name.LocalName;
                            if (localName == "xmlns")
                            {
                                localName = string.Empty;
                            }
                            if (!namespaceManager!.HasNamespace(localName))
                            {
                                namespaceManager.AddNamespace(localName, a.Value);
                            }
                        }
                    } while (a != e.lastAttr);
                }
                e = e.parent as XElement;
            }
        }
 
        private void PushElement(XElement e, ref string? xsiType, ref string? xsiNil)
        {
            namespaceManager!.PushScope();
            XAttribute? a = e.lastAttr;
            if (a != null)
            {
                do
                {
                    a = a.next!;
                    if (a.IsNamespaceDeclaration)
                    {
                        string localName = a.Name.LocalName;
                        if (localName == "xmlns")
                        {
                            localName = string.Empty;
                        }
                        namespaceManager.AddNamespace(localName, a.Value);
                    }
                    else
                    {
                        XName name = a.Name;
                        if (name == xsiTypeName)
                        {
                            xsiType = a.Value;
                        }
                        else if (name == xsiNilName)
                        {
                            xsiNil = a.Value;
                        }
                    }
                } while (a != e.lastAttr);
            }
        }
 
        private IXmlLineInfo SaveLineInfo(XObject? source)
        {
            IXmlLineInfo previousLineInfo = validator!.LineInfoProvider;
            validator.LineInfoProvider = source as IXmlLineInfo;
            return previousLineInfo;
        }
 
        private void RestoreLineInfo(IXmlLineInfo originalLineInfo)
        {
            validator!.LineInfoProvider = originalLineInfo;
        }
 
 
        private void ValidateAttribute(XAttribute a)
        {
            IXmlLineInfo original = SaveLineInfo(a);
            XmlSchemaInfo? si = addSchemaInfo ? new XmlSchemaInfo() : null;
            source = a;
            validator!.ValidateAttribute(a.Name.LocalName, a.Name.NamespaceName, a.Value, si);
            if (addSchemaInfo)
            {
                ReplaceSchemaInfo(a, si!);
            }
            RestoreLineInfo(original);
        }
 
        private void ValidateAttributes(XElement e)
        {
            XAttribute? a = e.lastAttr;
            IXmlLineInfo original = SaveLineInfo(a);
            if (a != null)
            {
                do
                {
                    a = a.next!;
                    if (!a.IsNamespaceDeclaration)
                    {
                        ValidateAttribute(a);
                    }
                } while (a != e.lastAttr);
                source = e;
            }
            if (addSchemaInfo)
            {
                if (defaultAttributes == null)
                {
                    defaultAttributes = new ArrayList();
                }
                else
                {
                    defaultAttributes.Clear();
                }
                validator!.GetUnspecifiedDefaultAttributes(defaultAttributes);
                foreach (XmlSchemaAttribute sa in defaultAttributes)
                {
                    a = new XAttribute(XNamespace.Get(sa.QualifiedName.Namespace).GetName(sa.QualifiedName.Name), GetDefaultValue(sa)!);
                    ReplaceSchemaInfo(a, GetDefaultAttributeSchemaInfo(sa));
                    e.Add(a);
                }
            }
            RestoreLineInfo(original);
        }
 
        private void ValidateElement(XElement e)
        {
            XmlSchemaInfo? si = addSchemaInfo ? new XmlSchemaInfo() : null;
            string? xsiType = null;
            string? xsiNil = null;
            PushElement(e, ref xsiType, ref xsiNil);
            IXmlLineInfo original = SaveLineInfo(e);
            source = e;
            validator!.ValidateElement(e.Name.LocalName, e.Name.NamespaceName, si, xsiType, xsiNil, null, null);
            ValidateAttributes(e);
            validator.ValidateEndOfAttributes(si);
            ValidateNodes(e);
            validator.ValidateEndElement(si);
            if (addSchemaInfo)
            {
                if (si!.Validity == XmlSchemaValidity.Valid && si.IsDefault)
                {
                    Debug.Assert(si.SchemaElement != null);
                    e.Value = GetDefaultValue(si.SchemaElement)!;
                }
                ReplaceSchemaInfo(e, si);
            }
            RestoreLineInfo(original);
            namespaceManager!.PopScope();
        }
 
        private void ValidateNodes(XElement e)
        {
            XNode? n = e.content as XNode;
            IXmlLineInfo original = SaveLineInfo(n);
            if (n != null)
            {
                do
                {
                    n = n.next!;
                    XElement? c = n as XElement;
                    if (c != null)
                    {
                        ValidateElement(c);
                    }
                    else
                    {
                        XText? t = n as XText;
                        if (t != null)
                        {
                            string s = t.Value;
                            if (s.Length > 0)
                            {
                                validator!.LineInfoProvider = t as IXmlLineInfo;
                                validator.ValidateText(s);
                            }
                        }
                    }
                } while (n != e.content);
                source = e;
            }
            else
            {
                string? s = e.content as string;
                if (s != null && s.Length > 0)
                {
                    validator!.ValidateText(s);
                }
            }
            RestoreLineInfo(original);
        }
 
        private void ValidationCallback(object? sender, ValidationEventArgs e)
        {
            if (validationEventHandler != null)
            {
                validationEventHandler(source, e);
            }
            else if (e.Severity == XmlSeverityType.Error)
            {
                throw e.Exception;
            }
        }
    }
 
    internal sealed class XmlSchemaInfoEqualityComparer : IEqualityComparer<XmlSchemaInfo>
    {
        public bool Equals(XmlSchemaInfo? si1, XmlSchemaInfo? si2)
        {
            if (si1 == si2) return true;
            if (si1 == null || si2 == null) return false;
            return si1.ContentType == si2.ContentType &&
                   si1.IsDefault == si2.IsDefault &&
                   si1.IsNil == si2.IsNil &&
                   (object?)si1.MemberType == (object?)si2.MemberType &&
                   (object?)si1.SchemaAttribute == (object?)si2.SchemaAttribute &&
                   (object?)si1.SchemaElement == (object?)si2.SchemaElement &&
                   (object?)si1.SchemaType == (object?)si2.SchemaType &&
                   si1.Validity == si2.Validity;
        }
 
        public int GetHashCode(XmlSchemaInfo? si)
        {
            if (si == null) return 0;
            int h = (int)si.ContentType;
            if (si.IsDefault)
            {
                h ^= 1;
            }
            if (si.IsNil)
            {
                h ^= 1;
            }
            XmlSchemaSimpleType? memberType = si.MemberType;
            if (memberType != null)
            {
                h ^= memberType.GetHashCode();
            }
            XmlSchemaAttribute? schemaAttribute = si.SchemaAttribute;
            if (schemaAttribute != null)
            {
                h ^= schemaAttribute.GetHashCode();
            }
            XmlSchemaElement? schemaElement = si.SchemaElement;
            if (schemaElement != null)
            {
                h ^= schemaElement.GetHashCode();
            }
            XmlSchemaType? schemaType = si.SchemaType;
            if (schemaType != null)
            {
                h ^= schemaType.GetHashCode();
            }
            h ^= (int)si.Validity;
            return h;
        }
    }
 
    /// <summary>
    /// Extension methods
    /// </summary>
    public static class Extensions
    {
        /// <summary>
        /// Gets the schema information that has been assigned to the <see cref="XElement"/> as a result of schema validation.
        /// </summary>
        /// <param name="source">Extension point</param>
        public static IXmlSchemaInfo? GetSchemaInfo(this XElement source)
        {
            ArgumentNullException.ThrowIfNull(source);
 
            return source.Annotation<IXmlSchemaInfo>();
        }
 
        /// <summary>
        /// Gets the schema information that has been assigned to the <see cref="XAttribute"/> as a result of schema validation.
        /// </summary>
        /// <param name="source">Extension point</param>
        public static IXmlSchemaInfo? GetSchemaInfo(this XAttribute source)
        {
            ArgumentNullException.ThrowIfNull(source);
 
            return source.Annotation<IXmlSchemaInfo>();
        }
 
        /// <summary>
        /// Validate a <see cref="XDocument"/>
        /// </summary>
        /// <param name="source">Extension point</param>
        /// <param name="schemas">The <see cref="XmlSchemaSet"/> used for validation</param>
        /// <param name="validationEventHandler">The <see cref="ValidationEventHandler"/>
        /// that receives schema validation warnings and errors encountered during schema
        /// validation</param>
        public static void Validate(this XDocument source, XmlSchemaSet schemas, ValidationEventHandler? validationEventHandler)
        {
            source.Validate(schemas, validationEventHandler, false);
        }
 
        /// <summary>
        /// Validate a <see cref="XDocument"/>
        /// </summary>
        /// <param name="source">Extension point</param>
        /// <param name="schemas">The <see cref="XmlSchemaSet"/> used for validation</param>
        /// <param name="validationEventHandler">The <see cref="ValidationEventHandler"/>
        /// that receives schema validation warnings and errors encountered during schema
        /// validation</param>
        /// <param name="addSchemaInfo">If enabled the <see cref="XDocument"/> and the corresponding
        /// subtree is augmented with PSVI in the form of <see cref="IXmlSchemaInfo"/> annotations,
        /// default attributes and default element values</param>
        public static void Validate(this XDocument source, XmlSchemaSet schemas, ValidationEventHandler? validationEventHandler, bool addSchemaInfo)
        {
            ArgumentNullException.ThrowIfNull(source);
            ArgumentNullException.ThrowIfNull(schemas);
 
            new XNodeValidator(schemas, validationEventHandler).Validate(source, null, addSchemaInfo);
        }
 
        /// <summary>
        /// Validate a <see cref="XElement"/>
        /// </summary>
        /// <param name="source">Extension point</param>
        /// <param name="partialValidationType">An <see cref="XmlSchemaElement"/> or
        /// <see cref="XmlSchemaType"/> object used to initialize the partial validation
        /// context</param>
        /// <param name="schemas">The <see cref="XmlSchemaSet"/> used for validation</param>
        /// <param name="validationEventHandler">The <see cref="ValidationEventHandler"/> that
        /// receives schema validation warnings and errors encountered during schema
        /// validation</param>
        public static void Validate(this XElement source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler? validationEventHandler)
        {
            source.Validate(partialValidationType, schemas, validationEventHandler, false);
        }
 
        /// <summary>
        /// Validate a <see cref="XElement"/>
        /// </summary>
        /// <param name="source">Extension point</param>
        /// <param name="partialValidationType">An <see cref="XmlSchemaElement"/> or
        /// <see cref="XmlSchemaType"/> object used to initialize the partial validation
        /// context</param>
        /// <param name="schemas">The <see cref="XmlSchemaSet"/> used for validation</param>
        /// <param name="validationEventHandler">The <see cref="ValidationEventHandler"/> that
        /// receives schema validation warnings and errors encountered during schema
        /// validation</param>
        /// <param name="addSchemaInfo">If enabled the <see cref="XElement"/> and the corresponding
        /// subtree is augmented with PSVI in the form of <see cref="IXmlSchemaInfo"/> annotations,
        /// default attributes and default element values</param>
        public static void Validate(this XElement source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler? validationEventHandler, bool addSchemaInfo)
        {
            ArgumentNullException.ThrowIfNull(source);
            ArgumentNullException.ThrowIfNull(partialValidationType);
            ArgumentNullException.ThrowIfNull(schemas);
 
            new XNodeValidator(schemas, validationEventHandler).Validate(source, partialValidationType, addSchemaInfo);
        }
 
        /// <summary>
        /// Validate a <see cref="XAttribute"/>
        /// </summary>
        /// <param name="source">Extension point</param>
        /// <param name="partialValidationType">An <see cref="XmlSchemaAttribute"/> or
        /// <see cref="XmlSchemaType"/> object used to initialize the partial validation
        /// context</param>
        /// <param name="schemas">The <see cref="XmlSchemaSet"/> used for validation</param>
        /// <param name="validationEventHandler">The <see cref="ValidationEventHandler"/> that
        /// receives schema validation warnings and errors encountered during schema
        /// validation</param>
        public static void Validate(this XAttribute source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler? validationEventHandler)
        {
            source.Validate(partialValidationType, schemas, validationEventHandler, false);
        }
 
        /// <summary>
        /// Validate a <see cref="XAttribute"/>
        /// </summary>
        /// <param name="source">Extension point</param>
        /// <param name="partialValidationType">An <see cref="XmlSchemaAttribute"/> or
        /// <see cref="XmlSchemaType"/> object used to initialize the partial validation
        /// context</param>
        /// <param name="schemas">The <see cref="XmlSchemaSet"/> used for validation</param>
        /// <param name="validationEventHandler">The <see cref="ValidationEventHandler"/> that
        /// receives schema validation warnings and errors encountered during schema
        /// validation</param>
        /// <param name="addSchemaInfo">If enabled the <see cref="XAttribute"/> is augmented with PSVI
        /// in the form of <see cref="IXmlSchemaInfo"/> annotations, default attributes and
        /// default element values</param>
        public static void Validate(this XAttribute source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler? validationEventHandler, bool addSchemaInfo)
        {
            ArgumentNullException.ThrowIfNull(source);
            ArgumentNullException.ThrowIfNull(partialValidationType);
            ArgumentNullException.ThrowIfNull(schemas);
 
            new XNodeValidator(schemas, validationEventHandler).Validate(source, partialValidationType, addSchemaInfo);
        }
    }
}