File: XmlTransforms.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.Diagnostics;
using RegularExpressions = System.Text.RegularExpressions;
using Microsoft.Web.XmlTransform.Properties;

namespace Microsoft.Web.XmlTransform
{
    internal static class CommonErrors
    {
        internal static void ExpectNoArguments(XmlTransformationLogger log, string transformName, string argumentString) {
            if (!String.IsNullOrEmpty(argumentString)) {
                log.LogWarning(Resources.XMLTRANSFORMATION_TransformDoesNotExpectArguments, transformName);
            }
        }

        internal static void WarnIfMultipleTargets(XmlTransformationLogger log, string transformName, XmlNodeList targetNodes, bool applyTransformToAllTargets) {
            Debug.Assert(applyTransformToAllTargets == false);

            if (targetNodes.Count > 1) {
                log.LogWarning(Resources.XMLTRANSFORMATION_TransformOnlyAppliesOnce, transformName);
            }
        }
    }

    internal class Replace : Transform
    {
        protected override void Apply() {
            CommonErrors.ExpectNoArguments(Log, TransformNameShort, ArgumentString);
            CommonErrors.WarnIfMultipleTargets(Log, TransformNameShort, TargetNodes, ApplyTransformToAllTargetNodes);

            XmlNode parentNode = TargetNode.ParentNode;
            parentNode.ReplaceChild(
                TransformNode,
                TargetNode);

            Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageReplace, TargetNode.Name);
        }
    }


    internal class Remove : Transform
    {
        protected override void Apply() {
            CommonErrors.WarnIfMultipleTargets(Log, TransformNameShort, TargetNodes, ApplyTransformToAllTargetNodes);

            RemoveNode();
        }

        protected void RemoveNode() {
            CommonErrors.ExpectNoArguments(Log, TransformNameShort, ArgumentString);

            XmlNode parentNode = TargetNode.ParentNode;
            parentNode.RemoveChild(TargetNode);

            Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageRemove, TargetNode.Name);
        }
    }

    internal class RemoveAll : Remove
    {
        public RemoveAll() {
            ApplyTransformToAllTargetNodes = true;
        }

        protected override void Apply() {
            RemoveNode();
        }
    }

    internal class Insert : Transform
    {
        public Insert()
            : base(TransformFlags.UseParentAsTargetNode, MissingTargetMessage.Error) {
        }

        protected override void Apply() {
            CommonErrors.ExpectNoArguments(Log, TransformNameShort, ArgumentString);

            TargetNode.AppendChild(TransformNode);

            Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageInsert, TransformNode.Name);
        }
    }

    internal class InsertIfMissing : Insert
    {
        protected override void Apply()
        {
            CommonErrors.ExpectNoArguments(Log, TransformNameShort, ArgumentString);
            if (this.TargetChildNodes == null || this.TargetChildNodes.Count == 0)
            {
                TargetNode.AppendChild(TransformNode);
                Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageInsert, TransformNode.Name);
            }
        }
    }



    internal abstract class InsertBase : Transform
    {
        internal InsertBase()
            : base(TransformFlags.UseParentAsTargetNode, MissingTargetMessage.Error) {
        }

        private XmlElement siblingElement = null;

        protected XmlElement SiblingElement {
            get {
                if (siblingElement == null) {
                    if (Arguments == null || Arguments.Count == 0) {
                        throw new XmlTransformationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_InsertMissingArgument, GetType().Name));
                    }
                    else if (Arguments.Count > 1) {
                        throw new XmlTransformationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_InsertTooManyArguments, GetType().Name));
                    }
                    else {
                        string xpath = Arguments[0];
                        XmlNodeList siblings = TargetNode.SelectNodes(xpath);
                        if (siblings.Count == 0) {
                            throw new XmlTransformationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_InsertBadXPath, xpath));
                        }
                        else {
                            siblingElement = siblings[0] as XmlElement;
                            if (siblingElement == null) {
                                throw new XmlTransformationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_InsertBadXPathResult, xpath));
                            }
                        }
                    }
                }

                return siblingElement;
            }
        }
    }

    internal class InsertAfter : InsertBase
    {
        protected override void Apply() {
            SiblingElement.ParentNode.InsertAfter(TransformNode, SiblingElement);

            Log.LogMessage(MessageType.Verbose, string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_TransformMessageInsert, TransformNode.Name));
        }
    }

    internal class InsertBefore : InsertBase
    {
        protected override void Apply() {
            SiblingElement.ParentNode.InsertBefore(TransformNode, SiblingElement);

            Log.LogMessage(MessageType.Verbose, string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_TransformMessageInsert, TransformNode.Name));
        }
    }

    public class SetAttributes : AttributeTransform
    {
        protected override void Apply() {
            foreach (XmlAttribute transformAttribute in TransformAttributes) {
                XmlAttribute targetAttribute = TargetNode.Attributes.GetNamedItem(transformAttribute.Name) as XmlAttribute;
                if (targetAttribute != null) {
                    targetAttribute.Value = transformAttribute.Value;
                }
                else {
                    TargetNode.Attributes.Append((XmlAttribute)transformAttribute.Clone());
                }

                Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageSetAttribute, transformAttribute.Name);
            }

            if (TransformAttributes.Count > 0) {
                Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageSetAttributes, TransformAttributes.Count);
            }
            else {
                Log.LogWarning(Resources.XMLTRANSFORMATION_TransformMessageNoSetAttributes);
            }
        }
    }

    public class CommentOut : Transform
    {
        protected override void Apply()
        {
            XmlNode parentNode = TargetNode.ParentNode;
            XmlDocument document = TargetNode.OwnerDocument;
            if (parentNode == null || document == null)
            {
                throw new XmlTransformationException(Resources.XMLTRANSFORMATION_InvalidCommentOutTarget);
            }

            string outerXml = $" {TargetNode.OuterXml} ";
            XmlComment comment = document.CreateComment(outerXml);
            parentNode.InsertAfter(comment, TargetNode);
            parentNode.RemoveChild(TargetNode);

            Log.LogMessage(MessageType.Verbose, string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_TransformMessageCommentOut, TargetNode.Name));
        }
    }
    

    public class SetTokenizedAttributeStorage
    {
        public List<Dictionary<string, string>> DictionaryList { get; set; }
        public string TokenFormat { get; set; }
        public bool EnableTokenizeParameters { get; set; }
        public bool UseXpathToFormParameter { get; set; }
        public SetTokenizedAttributeStorage() : this(4) { }
        public SetTokenizedAttributeStorage(int capacity)
        {
            DictionaryList = new List<Dictionary<string, string>>(capacity);
            TokenFormat = string.Concat("$(ReplacableToken_#(", SetTokenizedAttributes.ParameterAttribute, ")_#(", SetTokenizedAttributes.TokenNumber, "))");
            EnableTokenizeParameters = false;
            UseXpathToFormParameter = true;
        }
    }

    /// <summary>
    /// Utility class to Transform the SetAttribute to replace token
    /// 1. if it trigger by the regular TransformXml task, it only replace the $(name) from the parent node
    /// 2. If it trigger by the TokenizedTransformXml task, it replace $(name) then parse the declareation of the parameter
    /// </summary>
    public class SetTokenizedAttributes : AttributeTransform
    {

        private SetTokenizedAttributeStorage storageDictionary = null;
        private bool fInitStorageDictionary = false;
        public static readonly string Token = "Token";
        public static readonly string TokenNumber = "TokenNumber";
        public static readonly string XPathWithIndex = "XPathWithIndex";
        public static readonly string ParameterAttribute = "Parameter";
        public static readonly string XpathLocator = "XpathLocator";
        public static readonly string XPathWithLocator = "XPathWithLocator";

        private XmlAttribute tokenizeValueCurrentXmlAttribute = null;

    
        protected SetTokenizedAttributeStorage TransformStorage
        {
            get
            {
                if (storageDictionary == null && !fInitStorageDictionary)
                {
                    storageDictionary = GetService<SetTokenizedAttributeStorage>();
                    fInitStorageDictionary = true;
                }
                return storageDictionary;
            }
        }

        protected override void Apply()
        {
            bool fTokenizeParameter = false;
            SetTokenizedAttributeStorage storage = TransformStorage;
            List<Dictionary<string, string> > parameters = null;

            if (storage != null)
            {
                fTokenizeParameter = storage.EnableTokenizeParameters;
                if (fTokenizeParameter)
                {
                    parameters = storage.DictionaryList;
                }
            }

            foreach (XmlAttribute transformAttribute in TransformAttributes)
            {
                XmlAttribute targetAttribute = TargetNode.Attributes.GetNamedItem(transformAttribute.Name) as XmlAttribute;

                string newValue = TokenizeValue(targetAttribute, transformAttribute, fTokenizeParameter, parameters);

                if (targetAttribute != null)
                {
                    targetAttribute.Value = newValue;
                }
                else
                {
                    XmlAttribute newAttribute = (XmlAttribute)transformAttribute.Clone();
                    newAttribute.Value = newValue;
                    TargetNode.Attributes.Append(newAttribute);
                }

                Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageSetAttribute, transformAttribute.Name);
            }

            if (TransformAttributes.Count > 0)
            {
                Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageSetAttributes, TransformAttributes.Count);
            }
            else
            {
                Log.LogWarning(Resources.XMLTRANSFORMATION_TransformMessageNoSetAttributes);
            }
        }


        static private RegularExpressions.Regex s_dirRegex = null;
        static private RegularExpressions.Regex s_parentAttribRegex = null;
        static private RegularExpressions.Regex s_tokenFormatRegex = null;

        // Directory registrory
        static internal RegularExpressions.Regex DirRegex
        {
            get
            {
                if (s_dirRegex == null)
                {
                    s_dirRegex = new RegularExpressions.Regex(@"\G\{%(\s*(?<attrname>\w+(?=\W))(\s*(?<equal>=)\s*'(?<attrval>[^']*)'|\s*(?<equal>=)\s*(?<attrval>[^\s%>]*)|(?<equal>)(?<attrval>\s*?)))*\s*?%\}");
                }
                return s_dirRegex;
            }
        }

        static internal RegularExpressions.Regex ParentAttributeRegex
        {
            get
            {
                if (s_parentAttribRegex == null)
                {
                    s_parentAttribRegex = new RegularExpressions.Regex(@"\G\$\((?<tagname>[\w:\.]+)\)"); 
                }
                return s_parentAttribRegex;
            }
        }

        static internal RegularExpressions.Regex TokenFormatRegex
        {
            get
            {
                if (s_tokenFormatRegex == null)
                {
                    s_tokenFormatRegex = new RegularExpressions.Regex(@"\G\#\((?<tagname>[\w:\.]+)\)");
                }
                return s_tokenFormatRegex;
            }
        }

        protected delegate string GetValueCallback(string key);

        protected string GetAttributeValue(string attributeName)
        {
            string dataValue = null;
            XmlAttribute sourceAttribute = TargetNode.Attributes.GetNamedItem(attributeName) as XmlAttribute;
            if (sourceAttribute == null)
            {
                if (string.Compare(attributeName, tokenizeValueCurrentXmlAttribute.Name, StringComparison.OrdinalIgnoreCase) != 0)
                {   // if it is other attributename, we fall back to the current now 
                    sourceAttribute = TransformNode.Attributes.GetNamedItem(attributeName) as XmlAttribute;
                }
            }
            if (sourceAttribute != null)
            {
                dataValue = sourceAttribute.Value;
            }
            return dataValue;
        }


        //DirRegex treat single quote differently
        protected string EscapeDirRegexSpecialCharacter(string value, bool escape)
        {
            if (escape)
            {
                return value.Replace("'", "&apos;");
            }
            else
            {
                return value.Replace("&apos;", "'");
            }
        }


        protected static string SubstituteKownValue(string transformValue, RegularExpressions.Regex patternRegex, string patternPrefix,  GetValueCallback getValueDelegate )
        {
            int position = 0;
            List<RegularExpressions.Match> matchsExpr = new List<RegularExpressions.Match>();
            do
            {
                position = transformValue.IndexOf(patternPrefix, position, StringComparison.OrdinalIgnoreCase);
                if (position > -1)
                {
                    RegularExpressions.Match match = patternRegex.Match(transformValue, position);
                    // Add the successful match to collection
                    if (match.Success)
                    {
                        matchsExpr.Add(match);
                        position = match.Index + match.Length;
                    }
                    else
                    {
                        position++;
                    }
                }
            } while (position > -1);

            System.Text.StringBuilder strbuilder = new StringBuilder(transformValue.Length);
            if (matchsExpr.Count > 0)
            {
                strbuilder.Remove(0, strbuilder.Length);
                position = 0;
                int index = 0;
                foreach (RegularExpressions.Match match in matchsExpr)
                {
                    strbuilder.Append(transformValue.Substring(position, match.Index - position));
                    RegularExpressions.Capture captureTagName = match.Groups["tagname"];
                    string attributeName = captureTagName.Value;

                    string newValue = getValueDelegate(attributeName);

                    if (newValue != null) // null indicate that the attribute is not exist
                    {
                        strbuilder.Append(newValue);
                    }
                    else
                    {
                        // keep original value
                        strbuilder.Append(match.Value);
                    }
                    position = match.Index + match.Length;
                    index++;
                }
                strbuilder.Append(transformValue.Substring(position));

                transformValue = strbuilder.ToString();
            }

            return transformValue;
        }

        private string GetXPathToAttribute(XmlAttribute xmlAttribute)
        {
            return GetXPathToAttribute(xmlAttribute, null);
        }

        private string GetXPathToAttribute(XmlAttribute xmlAttribute, IList<string> locators)
        {
            string path = string.Empty;
            if (xmlAttribute != null)
            {
                string pathToNode = GetXPathToNode(xmlAttribute.OwnerElement);
                if (!string.IsNullOrEmpty(pathToNode))
                {
                    System.Text.StringBuilder identifier = new StringBuilder(256);
                    if (!(locators == null || locators.Count == 0))
                    {
                        foreach (string match in locators)
                        {
                            string val = this.GetAttributeValue(match);
                            if (!string.IsNullOrEmpty(val))
                            {
                                if (identifier.Length != 0)
                                {
                                    identifier.Append(" and ");
                                }
                                identifier.Append(String.Format(System.Globalization.CultureInfo.InvariantCulture, "@{0}='{1}'", match, val));
                            }
                            else
                            {
                                throw new XmlTransformationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,Resources.XMLTRANSFORMATION_MatchAttributeDoesNotExist, match));
                            }
                        }
                    }

                    if (identifier.Length == 0) 
                    {
                        for (int i = 0; i < TargetNodes.Count; i++)
                        {
                            if (TargetNodes[i] == xmlAttribute.OwnerElement)
                            {
                                // Xpath is 1 based
                                identifier.Append((i + 1).ToString(System.Globalization.CultureInfo.InvariantCulture));
                                break;
                            }
                        }
                    }
                    pathToNode = string.Concat(pathToNode, "[", identifier.ToString(), "]");
                }
                path = string.Concat(pathToNode, "/@", xmlAttribute.Name);
            }
            return path;
        }

        private string GetXPathToNode(XmlNode xmlNode)
        {
            if (xmlNode == null || xmlNode.NodeType == XmlNodeType.Document)
            {
                return null;
            }
            string parentPath = GetXPathToNode(xmlNode.ParentNode);
            return string.Concat(parentPath, "/", xmlNode.Name);
        }

        private string TokenizeValue(XmlAttribute targetAttribute, 
                                     XmlAttribute transformAttribute, 
                                     bool fTokenizeParameter, 
                                     List<Dictionary<string, string>> parameters)
        {
            Debug.Assert(!fTokenizeParameter || parameters != null);

            tokenizeValueCurrentXmlAttribute = transformAttribute;
            string transformValue = transformAttribute.Value;
            string xpath = GetXPathToAttribute(targetAttribute);

            //subsitute the know value first in the transformAttribute
            transformValue = SubstituteKownValue(transformValue, ParentAttributeRegex, "$(", delegate(string key) { return EscapeDirRegexSpecialCharacter(GetAttributeValue(key), true); });

            // then use the directive to parse the value. --- if TokenizeParameterize is enable
            if (fTokenizeParameter && parameters != null)
            {
                int position = 0;
                System.Text.StringBuilder strbuilder = new StringBuilder(transformValue.Length);
                position = 0;
                List<RegularExpressions.Match> matchs = new List<RegularExpressions.Match>();

                do
                {
                    position = transformValue.IndexOf("{%", position, StringComparison.OrdinalIgnoreCase);
                    if (position > -1)
                    {
                        RegularExpressions.Match match = DirRegex.Match(transformValue, position);
                        // Add the successful match to collection
                        if (match.Success)
                        {
                            matchs.Add(match);
                            position = match.Index + match.Length;
                        }
                        else
                        {
                            position++;
                        }
                    }
                } while (position > -1);

                if (matchs.Count > 0)
                {
                    strbuilder.Remove(0, strbuilder.Length);
                    position = 0;
                    int index = 0;

                    foreach (RegularExpressions.Match match in matchs)
                    {
                        strbuilder.Append(transformValue.Substring(position, match.Index - position));
                        RegularExpressions.CaptureCollection attrnames = match.Groups["attrname"].Captures;
                        if (attrnames != null && attrnames.Count > 0)
                        {
                            RegularExpressions.CaptureCollection attrvalues = match.Groups["attrval"].Captures;
                            Dictionary<string, string> paramDictionary = new Dictionary<string, string>(4, StringComparer.OrdinalIgnoreCase);

                            paramDictionary[XPathWithIndex] = xpath;
                            paramDictionary[TokenNumber] = index.ToString(System.Globalization.CultureInfo.InvariantCulture);

                            // Get the key-value pare of the in the tranform form
                            for (int i = 0; i < attrnames.Count; i++)
                            {
                                string name = attrnames[i].Value;
                                string val = null;
                                if (attrvalues != null && i < attrvalues.Count)
                                {
                                    val = EscapeDirRegexSpecialCharacter(attrvalues[i].Value, false);
                                }
                                paramDictionary[name] = val;
                            }

                            //Identify the Token format
                            string strTokenFormat = null;
                            if (!paramDictionary.TryGetValue(Token, out strTokenFormat))
                            {
                                strTokenFormat = storageDictionary.TokenFormat;
                            }
                            if (!string.IsNullOrEmpty(strTokenFormat))
                            {
                                paramDictionary[Token] = strTokenFormat;
                            }

                            // Second translation of #() -- replace with the existing Parameters
                            int count = paramDictionary.Count;
                            string[] keys = new string[count];
                            paramDictionary.Keys.CopyTo(keys, 0);
                            for (int i = 0; i < count; i++)
                            {
                                // if token format contain the #(),we replace with the known value such that it is unique identify
                                // for example, intokenizeTransformXml.cs, default token format is
                                // string.Concat("$(ReplacableToken_#(", SetTokenizedAttributes.ParameterAttribute, ")_#(", SetTokenizedAttributes.TokenNumber, "))");
                                // which ParameterAttribute will be translate to parameterDictionary["parameter"} and TokenNumber will be translate to parameter 
                                // parameterDictionary["TokenNumber"]
                                string keyindex = keys[i];
                                string val = paramDictionary[keyindex];
                                string newVal = SubstituteKownValue(val, TokenFormatRegex, "#(",
                                        delegate(string key) { return paramDictionary.ContainsKey(key) ? paramDictionary[key] : null; });

                                paramDictionary[keyindex] = newVal;
                            }

                            if (paramDictionary.TryGetValue(Token, out strTokenFormat))
                            {
                                // Replace with token
                                strbuilder.Append(strTokenFormat);
                            }
                            string attributeLocator;
                            if (paramDictionary.TryGetValue(XpathLocator, out attributeLocator) && !string.IsNullOrEmpty(attributeLocator))
                            {
                                IList<string> locators =  XmlArgumentUtility.SplitArguments(attributeLocator);
                                string xpathwithlocator = GetXPathToAttribute(targetAttribute,locators);
                                if (!string.IsNullOrEmpty(xpathwithlocator))
                                {
                                    paramDictionary[XPathWithLocator] = xpathwithlocator;
                                }
                            }
                            parameters.Add(paramDictionary);
                        }

                        position = match.Index + match.Length;
                        index++;
                    }
                    strbuilder.Append(transformValue.Substring(position));
                    transformValue = strbuilder.ToString();
                }
            }
            return transformValue;
        }

    }

    public class RemoveAttributes : AttributeTransform
    {
        protected override void Apply() {
            foreach (XmlAttribute attribute in TargetAttributes) {
                TargetNode.Attributes.Remove(attribute);

                Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageRemoveAttribute, attribute.Name);
            }

            if (TargetAttributes.Count > 0) {
                Log.LogMessage(MessageType.Verbose, Resources.XMLTRANSFORMATION_TransformMessageRemoveAttributes, TargetAttributes.Count);
            }
            else {
                Log.LogWarning(TargetNode, Resources.XMLTRANSFORMATION_TransformMessageNoRemoveAttributes);
            }
        }
    }
}