File: XmlElementContext.cs
Web Access
Project: src\src\xdt\src\Microsoft.Web.XmlTransform\Microsoft.Web.XmlTransform.csproj (Microsoft.Web.XmlTransform)
// 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.Generic;
using System.Text;
using System.Xml;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.IO;
using System.Globalization;
using Microsoft.Web.XmlTransform.Properties;

namespace Microsoft.Web.XmlTransform
{
    internal class XmlElementContext : XmlNodeContext
    {
        #region private data members
        private XmlElementContext parentContext;
        private string xpath = null;
        private string parentXPath = null;
        private XmlDocument xmlTargetDoc;

        private IServiceProvider serviceProvider;

        private XmlNode transformNodes = null;
        private XmlNodeList targetNodes = null;
        private XmlNodeList targetParents = null;

        private XmlAttribute transformAttribute = null;
        private XmlAttribute locatorAttribute = null;

        private XmlNamespaceManager namespaceManager = null;
        #endregion

        public XmlElementContext(XmlElementContext parent, XmlElement element, XmlDocument xmlTargetDoc, IServiceProvider serviceProvider)
            : base(element) {
            this.parentContext = parent;
            this.xmlTargetDoc = xmlTargetDoc;
            this.serviceProvider = serviceProvider;
        }

        public T GetService<T>() where T : class {
            if (serviceProvider != null) {
                T service = serviceProvider.GetService(typeof(T)) as T;
                // now it is legal to return service that's null -- due to SetTokenizeAttributeStorage
                //Debug.Assert(service != null, String.Format(CultureInfo.InvariantCulture, "Service provider didn't provide {0}", typeof(ServiceType).Name));
                return service;
            }
            else {
                Debug.Fail("No ServiceProvider");
                return null;
            }
        }

        #region data accessors
        public XmlElement Element {
            get {
                return Node as XmlElement;
            }
        }

        public string XPath {
            get {
                if (xpath == null) {
                    xpath = ConstructXPath();
                }
                return xpath;
            }
        }

        public string ParentXPath {
            get {
                if (parentXPath == null) {
                    parentXPath = ConstructParentXPath();
                }
                return parentXPath;
            }
        }

        public Transform ConstructTransform(out string argumentString) {
            try {
                return CreateObjectFromAttribute<Transform>(out argumentString, out transformAttribute);
            }
            catch (Exception ex) {
                throw WrapException(ex);
            }
        }

        public int TransformLineNumber {
            get {
                IXmlLineInfo lineInfo = transformAttribute as IXmlLineInfo;
                if (lineInfo != null) {
                    return lineInfo.LineNumber;
                }
                else {
                    return LineNumber;
                }
            }
        }

        public int TransformLinePosition {
            get {
                IXmlLineInfo lineInfo = transformAttribute as IXmlLineInfo;
                if (lineInfo != null) {
                    return lineInfo.LinePosition;
                }
                else {
                    return LinePosition;
                }
            }
        }

        public XmlAttribute TransformAttribute {
            get {
                return transformAttribute;
            }
        }

        public XmlAttribute LocatorAttribute {
            get {
                return locatorAttribute;
            }
        }
        #endregion

        #region XPath construction
        private string ConstructXPath() {
            try {
                string argumentString;
                string parentPath = parentContext == null ? String.Empty : parentContext.XPath;

                Locator locator = CreateLocator(out argumentString);

                return locator.ConstructPath(parentPath, this, argumentString);
            }
            catch (Exception ex) {
                throw WrapException(ex);
            }
        }

        private string ConstructParentXPath() {
            try {
                string argumentString;
                string parentPath = parentContext == null ? String.Empty : parentContext.XPath;

                Locator locator = CreateLocator(out argumentString);

                return locator.ConstructParentPath(parentPath, this, argumentString);
            }
            catch (Exception ex) {
                throw WrapException(ex);
            }
        }

        private Locator CreateLocator(out string argumentString) {
            Locator locator = CreateObjectFromAttribute<Locator>(out argumentString, out locatorAttribute);
            if (locator == null) {
                argumentString = null;
                //avoid using singleton of "DefaultLocator.Instance", so unit tests can run parallel
                locator = new DefaultLocator();
            }
            return locator;
        }
        #endregion

        #region Context information
        internal XmlNode TransformNode {
            get {
                if (transformNodes == null) {
                    transformNodes = CreateCloneInTargetDocument(Element);
                }
                return transformNodes;
            }
        }

        internal XmlNodeList TargetNodes {
            get {
                if (targetNodes == null) {
                    targetNodes = GetTargetNodes(XPath);
                }
                return targetNodes;
            }
        }

        internal XmlNodeList TargetParents {
            get {
                if (targetParents == null && parentContext != null) {
                    targetParents = GetTargetNodes(ParentXPath);
                }
                return targetParents;
            }
        }
        #endregion

        #region Node helpers
        private XmlDocument TargetDocument {
            get {
                return xmlTargetDoc;
            }
        }

        private XmlNode CreateCloneInTargetDocument(XmlNode sourceNode) {
            XmlFileInfoDocument infoDocument = TargetDocument as XmlFileInfoDocument;
            XmlNode clonedNode;
            
            if (infoDocument != null) {
                clonedNode = infoDocument.CloneNodeFromOtherDocument(sourceNode);
            }
            else {
                XmlReader reader = new XmlTextReader(new StringReader(sourceNode.OuterXml));
                clonedNode = TargetDocument.ReadNode(reader);
            }

            ScrubTransformAttributesAndNamespaces(clonedNode);

            return clonedNode;
        }

        private void ScrubTransformAttributesAndNamespaces(XmlNode node) {
            if (node.Attributes != null) {
                List<XmlAttribute> attributesToRemove = new List<XmlAttribute>();
                foreach (XmlAttribute attribute in node.Attributes) {
                    if (attribute.NamespaceURI == XmlTransformation.TransformNamespace) {
                        attributesToRemove.Add(attribute);
                    }
                    else if (attribute.Prefix.Equals("xmlns") || attribute.Name.Equals("xmlns")) {
                        attributesToRemove.Add(attribute);
                    }
                    else {
                        attribute.Prefix = null;
                    }
                }
                foreach (XmlAttribute attributeToRemove in attributesToRemove) {
                    node.Attributes.Remove(attributeToRemove);
                }
            }

            // Do the same recursively for child nodes
            foreach (XmlNode childNode in node.ChildNodes) {
                ScrubTransformAttributesAndNamespaces(childNode);
            }
        }

        private XmlNodeList GetTargetNodes(string xpath) {
            XmlNamespaceManager mgr = GetNamespaceManager();
            return TargetDocument.SelectNodes(xpath, GetNamespaceManager());
        }

        private Exception WrapException(Exception ex) {
            return XmlNodeException.Wrap(ex, Element);
        }

        private Exception WrapException(Exception ex, XmlNode node) {
            return XmlNodeException.Wrap(ex, node);
        }

        private XmlNamespaceManager GetNamespaceManager() {
            if (namespaceManager == null) {
                XmlNodeList localNamespaces = Element.SelectNodes("namespace::*");

                if (localNamespaces.Count > 0) {
                    namespaceManager = new XmlNamespaceManager(Element.OwnerDocument.NameTable);

                    foreach (XmlAttribute nsAttribute in localNamespaces) {
                        string prefix = String.Empty;
                        int index = nsAttribute.Name.IndexOf(':');
                        if (index >= 0) {
                            prefix = nsAttribute.Name.Substring(index + 1);
                        }
                        else {
                            prefix = "_defaultNamespace";
                        }

                        namespaceManager.AddNamespace(prefix, nsAttribute.Value);
                    }
                }
                else {
                    namespaceManager = new XmlNamespaceManager(GetParentNameTable());
                }
            }
            return namespaceManager;
        }

        private XmlNameTable GetParentNameTable() {
            if (parentContext == null) {
                return Element.OwnerDocument.NameTable;
            }
            else {
                return parentContext.GetNamespaceManager().NameTable;
            }
        }
        #endregion

        #region Named object creation
        private static Regex nameAndArgumentsRegex = null;
        private Regex NameAndArgumentsRegex {
            get {
                if (nameAndArgumentsRegex == null) {
                    nameAndArgumentsRegex = new Regex(@"\A\s*(?<name>\w+)(\s*\((?<arguments>.*)\))?\s*\Z", RegexOptions.Compiled|RegexOptions.Singleline);
                }
                return nameAndArgumentsRegex;
            }
        }

        private string ParseNameAndArguments(string name, out string arguments) {
            arguments = null;

            System.Text.RegularExpressions.Match match = NameAndArgumentsRegex.Match(name);
            if (match.Success) {
                if (match.Groups["arguments"].Success) {
                    CaptureCollection argumentCaptures = match.Groups["arguments"].Captures;
                    if (argumentCaptures.Count == 1 && !String.IsNullOrEmpty(argumentCaptures[0].Value)) {
                        arguments = argumentCaptures[0].Value;
                    }
                }

                return match.Groups["name"].Captures[0].Value;
            }
            else {
                throw new XmlTransformationException(Resources.XMLTRANSFORMATION_BadAttributeValue);
            }
        }

        private ObjectType CreateObjectFromAttribute<ObjectType>(out string argumentString, out XmlAttribute objectAttribute) where ObjectType : class {
            objectAttribute = Element.Attributes.GetNamedItem(typeof(ObjectType).Name, XmlTransformation.TransformNamespace) as XmlAttribute;
            try {
                if (objectAttribute != null) {
                    string typeName = ParseNameAndArguments(objectAttribute.Value, out argumentString);
                    if (!String.IsNullOrEmpty(typeName)) {
                        NamedTypeFactory factory = GetService<NamedTypeFactory>();
                        return factory.Construct<ObjectType>(typeName);
                    }
                }
            }
            catch (Exception ex) {
                throw WrapException(ex, objectAttribute);
            }

            argumentString = null;
            return null;
        }
        #endregion

        #region Error reporting helpers
        internal bool HasTargetNode(out XmlElementContext failedContext, out bool existedInOriginal) {
            failedContext = null;
            existedInOriginal = false;

            if (TargetNodes.Count == 0) {
                failedContext = this;
                while (failedContext.parentContext != null &&
                    failedContext.parentContext.TargetNodes.Count == 0) {

                    failedContext = failedContext.parentContext;
                }

                existedInOriginal = ExistedInOriginal(failedContext.XPath);
                return false;
            }

            return true;
        }

        internal bool HasTargetParent(out XmlElementContext failedContext, out bool existedInOriginal) {
            failedContext = null;
            existedInOriginal = false;

            if (TargetParents.Count == 0) {
                failedContext = this;
                while (failedContext.parentContext != null &&
                    !String.IsNullOrEmpty(failedContext.parentContext.ParentXPath) &&
                    failedContext.parentContext.TargetParents.Count == 0) {

                    failedContext = failedContext.parentContext;
                }

                existedInOriginal = ExistedInOriginal(failedContext.XPath);
                return false;
            }

            return true;
        }

        private bool ExistedInOriginal(string xpath) {
            IXmlOriginalDocumentService service = GetService<IXmlOriginalDocumentService>();
            if (service != null) {
                XmlNodeList nodeList = service.SelectNodes(xpath, GetNamespaceManager());
                if (nodeList != null && nodeList.Count > 0) {
                    return true;
                }
            }

            return false;
        }
        #endregion
    }
}