File: System\Data\Services\Design\Xml\XNodeSchemaApplier.cs
Project: ndp\fx\src\DataWeb\Design\System.Data.Services.Design.csproj (System.Data.Services.Design)
//---------------------------------------------------------------------
// <copyright file="XNodeSchemaApplier.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides a class used to make an XElement conform to a given
//      XML Schema.
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services.Design.Xml
{
    #region Namespaces.
 
    using System.Diagnostics;
    using System;
    using System.Linq;
    using System.Xml;
    using System.Xml.Schema;
    using System.Xml.Linq;
    using System.Collections.Generic;
    using System.Runtime.Versioning;
 
    #endregion Namespaces.
 
    /// <summary>Use this class to remove unexpected elements and attributes from an XDocument instance.</summary>
    internal class XNodeSchemaApplier
    {
        #region Private fields.
 
        /// <summary>Namespace manager for current scope.</summary>
        private readonly XmlNamespaceManager namespaceManager;
 
        /// <summary>Schemas used to predict expected elements and attributes.</summary>
        private readonly XmlSchemaSet schemas;
 
        /// <summary>XName for xsi:type.</summary>
        private readonly XName xsiTypeName;
 
        /// <summary>XName for xsi:nil.</summary>
        private readonly XName xsiNilName;
 
        /// <summary>Schema validator used to predict expected elements and attributes.</summary>
        private XmlSchemaValidator validator;
 
        #endregion Private fields.
 
        #region Constructors.
 
        /// <summary>Initializes a new <see cref="XNodeSchemaApplier"/> instance.</summary>
        /// <param name="schemas">Schemas to use to predict elements and attributes.</param>
        private XNodeSchemaApplier(XmlSchemaSet schemas)
        {
            Debug.Assert(schemas != null, "schemas != null");
 
            this.schemas = schemas;
            XNamespace xsi = XNamespace.Get("http://www.w3.org/2001/XMLSchema-instance");
            this.xsiTypeName = xsi.GetName("type");
            this.xsiNilName = xsi.GetName("nil");
            this.namespaceManager = new XmlNamespaceManager(schemas.NameTable);
        }
 
        #endregion Constructors.
 
        #region Internal methods.
 
        /// <summary>
        /// Appends <paramref name="element"/> to the specified <paramref name="list"/>, creating as necessary.
        /// </summary>
        /// <typeparam name="T">List element type.</typeparam>
        /// <param name="list">List to add the element to, possibly null on entry.</param>
        /// <param name="element">Element to add to the list.</param>
        internal static void AppendWithCreation<T>(ref List<T> list, T element)
        {
            if (list == null)
            {
                list = new List<T>();
            }
 
            list.Add(element);
        }
 
        /// <summary>
        /// Applies the specified <paramref name="schemas"/> to remove unexpected elements and attributes from the 
        /// given <paramref name="element"/>.
        /// </summary>
        /// <param name="schemas">Set of schemas to apply.</param>
        /// <param name="element">Document to remove elements and attributes from.</param>
        internal static void Apply(XmlSchemaSet schemas, XElement element)
        {
            Debug.Assert(schemas != null, "schemas != null");
            Debug.Assert(element != null, "document != null");
 
            XNodeSchemaApplier applier = new XNodeSchemaApplier(schemas);
            applier.Validate(element);
        }
 
        #endregion Internal methods.
 
        #region Private methods.
 
        /// <summary>Determines whether the specified <paramref name="element"/> is expected.</summary>
        /// <param name="element">Element to check.</param>
        /// <param name="elementName">
        /// <see cref="XmlQualifiedName"/> of the <paramref name="element"/> (passed to avoid recreation).
        /// </param>
        /// <param name="expected">Expected schema particle.</param>
        /// <returns>true if the element is expected; false otherwise.</returns>
        private static bool IsElementExpected(XElement element, XmlQualifiedName elementName, XmlSchemaParticle expected)
        {
            Debug.Assert(element != null, "element != null");
            Debug.Assert(elementName != null, "elementName != null");
            Debug.Assert(expected != null, "expected != null");
            Debug.Assert(
                ToQualifiedName(element.Name) == elementName,
                "ToQualifiedName(element.Name) == elementName -- otherwise the caller get the 'caching' wrong");
 
            // These are all possibilities for a particle.
            XmlSchemaGroupRef schemaGroupRef = expected as XmlSchemaGroupRef;
            XmlSchemaAny schemaAny = expected as XmlSchemaAny;
            XmlSchemaElement schemaElement = expected as XmlSchemaElement;
            XmlSchemaAll schemaAll = expected as XmlSchemaAll;
            XmlSchemaChoice schemaChoice = expected as XmlSchemaChoice;
            XmlSchemaSequence schemaSequence = expected as XmlSchemaSequence;
 
            Debug.Assert(schemaGroupRef == null, "schemaGroupRef == null -- the validator flattens this out as options.");
            Debug.Assert(schemaSequence == null, "schemaSequence == null -- the validator flattens this out and picks the right one in seq.");
            Debug.Assert(schemaAll == null, "schemaAll == null -- the validator flattens this out as options.");
            Debug.Assert(schemaChoice == null, "schemaChoice == null -- the validator flattens this out as options.");
 
            if (schemaAny != null)
            {
                Debug.Assert(
                    schemaAny.Namespace == "##other" || schemaAny.Namespace == "##any",
                    "schemaAny.Namespace == '##other' || '##any' -- otherwise CSDL XSD resource was changed");
                if (schemaAny.Namespace == "##any")
                {
                    return true;
                }
                else if (schemaAny.Namespace == "##other")
                {
                    string realElementNamespace = element.Name.NamespaceName;
                    if (realElementNamespace != GetTargetNamespace(expected))
                    {
                        return true;
                    }
                }
            }
 
            if (schemaElement != null)
            {
                if (schemaElement.QualifiedName == elementName)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        /// <summary>Gets the target namespace that applies to the specified <paramref name="schemaObject"/>.</summary>
        /// <param name="schemaObject">XML schema object for which to get target namespace.</param>
        /// <returns>Target namespace for the specified <paramref name="schemaObject"/> (never null).</returns>
        private static string GetTargetNamespace(XmlSchemaObject schemaObject)
        {
            Debug.Assert(schemaObject != null, "schemaObject != null");
 
            string result = null;
            do
            {
                XmlSchema schema = schemaObject as XmlSchema;
                if (schema != null)
                {
                    result = schema.TargetNamespace;
                    Debug.Assert(!String.IsNullOrEmpty(schema.TargetNamespace), "schema.TargetNamespace != null||'' -- otherwise this isn't CSDL");
                }
                else
                {
                    schemaObject = schemaObject.Parent;
                    Debug.Assert(schemaObject != null, "o != null -- otherwise the object isn't parented to a schema");
                }
            }
            while (result == null);
 
            return result;
        }
 
        /// <summary>Determines whether the specified <paramref name="element"/> is expected.</summary>
        /// <param name="element">Element to check.</param>
        /// <param name="expectedParticles">Expected schema particles (possibly empty).</param>
        /// <returns>true if the element is expected; false otherwise.</returns>
        private static bool IsElementExpected(XElement element, XmlSchemaParticle[] expectedParticles)
        {
            Debug.Assert(element != null, "element != null");
            Debug.Assert(expectedParticles != null, "expectedParticles != null");
 
            XmlQualifiedName elementName = ToQualifiedName(element.Name);
            foreach (var expected in expectedParticles)
            {
                if (IsElementExpected(element, elementName, expected))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        /// <summary>Determines whether the specified <paramref name="attribute"/> is expected.</summary>
        /// <param name="attribute">Attribute to check.</param>
        /// <param name="expectedAttributes">Expected attributes (possibly empty).</param>
        /// <param name="anyAttribute">anyAttribute schema for a complex type element (possibly null).</param>
        /// <returns>true if the attribute is expected; false otherwise.</returns>
        private static bool IsAttributeExpected(XAttribute attribute, XmlSchemaAnyAttribute anyAttribute, XmlSchemaAttribute[] expectedAttributes)
        {
            Debug.Assert(attribute != null, "attribute != null");
            Debug.Assert(expectedAttributes != null, "expectedAttributes != null");
            Debug.Assert(expectedAttributes.All(a => a.Form != XmlSchemaForm.Qualified), "expectedAttributes.All(a => a.Form != XmlSchemaForm.Qualified)");
 
            var name = ToQualifiedName(attribute.Name);
            if (name.Namespace.Length == 0)
            {
                foreach (var expected in expectedAttributes)
                {
                    if (expected.Name == name.Name)
                    {
                        return true;
                    }
                }
            }
 
            if (anyAttribute != null)
            {
                Debug.Assert(
                    anyAttribute.Namespace == "##any" || anyAttribute.Namespace == "##other",
                    "anyAttribute.Namespace == '##any' || '##other' -- otherwise CSDL XSD resource was changed");
                if (anyAttribute.Namespace == "##any")
                {
                    return true;
                }
                else
                {
                    string attributeNamespace = attribute.Name.NamespaceName;
                    if (attributeNamespace.Length > 0 && attributeNamespace != GetTargetNamespace(anyAttribute))
                    {
                        return true;
                    }
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Return the <see cref="XmlQualifiedName"/> representation of the specified <paramref name="name"/>.
        /// </summary>
        /// <param name="name">XML name to return.</param>
        /// <returns>An <see cref="XmlQualifiedName"/> that represents the given <paramref name="name"/>.</returns>
        private static XmlQualifiedName ToQualifiedName(XName name)
        {
            Debug.Assert(name != null, "name != null");
            return new XmlQualifiedName(name.LocalName, name.NamespaceName);
        }
 
        /// <summary>Validates the specified <paramref name="element"/> object.</summary>
        /// <param name="element">Source object for validation (must be an element).</param>
        private void Validate(XElement element)
        {
            Debug.Assert(element != null, "element != null");
 
            XmlSchemaValidationFlags validationFlags = XmlSchemaValidationFlags.AllowXmlAttributes;
            this.PushAncestorsAndSelf(element.Parent);
 
            validator = new XmlSchemaValidator(schemas.NameTable, schemas, namespaceManager, validationFlags);
            validator.XmlResolver = null;
            validator.Initialize();
            this.ValidateElement(element);
            validator.EndValidation();
        }
 
        /// <summary>Pushes the specifed <paramref name="element"/> namespaces and those of ancestors.</summary>
        /// <param name="element">Element to push from - possibly null.</param>
        /// <remarks>
        /// Pushing in reverse order (up the tree rather than down the tree) is OK, because we check that
        /// the namespace local name hasn't been added yet. Use <see cref="PushElement"/> as we go down
        /// the tree to push/pop as usual.
        /// </remarks>
        private void PushAncestorsAndSelf(XElement element)
        {
            while (element != null)
            {
                foreach (XAttribute attribute in element.Attributes())
                {
                    if (attribute.IsNamespaceDeclaration)
                    {
                        string localName = attribute.Name.LocalName;
                        if (localName == "xmlns")
                        {
                            localName = string.Empty;
                        }
 
                        if (!namespaceManager.HasNamespace(localName))
                        {
                            namespaceManager.AddNamespace(localName, attribute.Value);
                        }
                    }
                }
 
                element = element.Parent as XElement;
            }
        }
 
        /// <summary>Pushes the specifed <paramref name="element" /> namespaces and those of ancestors.</summary>
        /// <param name="element">Element to push.</param>
        /// <param name="xsiType">The value for xsi:type on this element.</param>
        /// <param name="xsiNil">The value for xsi:nil on this element.</param>
        private void PushElement(XElement element, ref string xsiType, ref string xsiNil)
        {
            Debug.Assert(element != null, "e != null");
            namespaceManager.PushScope();
            foreach (XAttribute attribute in element.Attributes())
            {
                if (attribute.IsNamespaceDeclaration)
                {
                    string localName = attribute.Name.LocalName;
                    if (localName == "xmlns")
                    {
                        localName = string.Empty;
                    }
 
                    namespaceManager.AddNamespace(localName, attribute.Value);
                }
                else
                {
                    XName name = attribute.Name;
                    if (name == xsiTypeName)
                    {
                        xsiType = attribute.Value;
                    }
                    else if (name == xsiNilName)
                    {
                        xsiNil = attribute.Value;
                    }
                }
            }
        }
 
        /// <summary>Validates all attributes on the specified <paramref name="element"/>.</summary>
        /// <param name="element">Element to validate attributes on.</param>
        private void ValidateAttributes(XElement element)
        {
            Debug.Assert(element != null, "e != null");
 
            foreach (XAttribute attribute in element.Attributes())
            {
                if (!attribute.IsNamespaceDeclaration)
                {
                    validator.ValidateAttribute(attribute.Name.LocalName, attribute.Name.NamespaceName, attribute.Value, null);
                }
            }
        }
 
        //// SxS: This method does not expose any resources to the caller and passes null as resource names to 
        //// XmlSchemaValidator.ValidateElement (annotated with ResourceExposure(ResourceScope.None).
        //// It's OK to suppress the SxS warning.
        [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
        [ResourceExposure(ResourceScope.None)]
        private void ValidateElement(XElement e)
        {
            Debug.Assert(e != null, "e != null");
 
            XmlSchemaInfo schemaInfo = new XmlSchemaInfo();
            string xsiType = null;
            string xsiNil = null;
            this.PushElement(e, ref xsiType, ref xsiNil);
 
            // The current element is always valid - otherwise we wouldn't have recursed into it in the first place.
            validator.ValidateElement(e.Name.LocalName, e.Name.NamespaceName, schemaInfo, xsiType, xsiNil, null, null);
 
            // When we have no schema element, then e was included but we don't know about it - it's an extension 
            // element, likely under CSDL documentation. We'll skip the whole thing in this case.
            if (schemaInfo.SchemaElement != null)
            {
                XmlSchemaComplexType schemaComplexType = schemaInfo.SchemaElement.ElementSchemaType as XmlSchemaComplexType;
                this.TrimAttributes(e, (schemaComplexType == null) ? null : schemaComplexType.AttributeWildcard);
                this.ValidateAttributes(e);
                validator.ValidateEndOfAttributes(null);
 
                this.TrimAndValidateNodes(e);
            }
 
            validator.ValidateEndElement(null);
            this.namespaceManager.PopScope();
        }
 
        /// <summary>Removes attributes from the specified <paramref name="element"/> if they're unexpected.</summary>
        /// <param name="element">Element to remove attributes from.</param>
        /// <param name="anyAttribute">anyAttribute schema for a complex type element (possibly null).</param>
        private void TrimAttributes(XElement element, XmlSchemaAnyAttribute anyAttribute)
        {
            Debug.Assert(element != null, "e != null");
 
            List<XAttribute> unexpectedAttributes = null;
            var expectedAttributes = validator.GetExpectedAttributes();
            foreach (XAttribute attribute in element.Attributes())
            {
                if (attribute.IsNamespaceDeclaration)
                {
                    continue;
                }
 
                if (!IsAttributeExpected(attribute, anyAttribute, expectedAttributes))
                {
                    AppendWithCreation(ref unexpectedAttributes, attribute);
                }
            }
 
            if (unexpectedAttributes != null)
            {
                foreach (var attribute in unexpectedAttributes)
                {
                    attribute.Remove();
                }
            }
        }
 
        /// <summary>
        /// Removes nodes from the specified <paramref name="parent"/> element and validates its nodes.
        /// </summary>
        /// <param name="parent"></param>
        /// <remarks>
        /// While it's cleaner to do this in two passes, trim then validate, like we do with attributes, we need to
        /// validate as we go for the validator to return sequence elements in the right order.
        /// </remarks>
        private void TrimAndValidateNodes(XElement parent)
        {
            Debug.Assert(parent != null, "parent != null");
 
            List<XNode> unexpectedNodes = null;
            XmlSchemaParticle[] expectedParticles = null;
            foreach (XNode node in parent.Nodes())
            {
                // expectedParticles will be null the first iteration and right after we validate,
                // when we potentially have something different to validate against.
                if (expectedParticles == null)
                {
                    expectedParticles = validator.GetExpectedParticles();
                }
 
                Debug.Assert(expectedParticles != null, "expectedParticles != null -- GetExpectedParticles should return empty at worst");
                XElement element = node as XElement;
                if (element != null)
                {
                    if (!IsElementExpected(element, expectedParticles))
                    {
                        AppendWithCreation(ref unexpectedNodes, element);
                    }
                    else
                    {
                        this.ValidateElement(element);
                        expectedParticles = null;
                    }
                }
                else
                {
                    XText text = node as XText;
                    if (text != null)
                    {
                        string s = text.Value;
                        if (s.Length > 0)
                        {
                            validator.ValidateText(s);
                            expectedParticles = null;
                        }
                    }
                }
            }
 
            if (unexpectedNodes != null)
            {
                foreach (var node in unexpectedNodes)
                {
                    node.Remove();
                }
            }
        }
 
        #endregion Private methods.
    }
}