| /******************************************************************************* | |
| * You may amend and distribute as you like, but don't remove this header! | |
| * | |
| * EPPlus provides server-side generation of Excel 2007/2010 spreadsheets. | |
| * See http://www.codeplex.com/EPPlus for details. | |
| * | |
| * Copyright (C) 2011 Jan Källman | |
| * | |
| * This library is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU Lesser General Public | |
| * License as published by the Free Software Foundation; either | |
| * version 2.1 of the License, or (at your option) any later version. | |
| * This library is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
| * See the GNU Lesser General Public License for more details. | |
| * | |
| * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php | |
| * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html | |
| * | |
| * All code and executables are provided "as is" with no warranty either express or implied. | |
| * The author accepts no liability for any damage or loss of business that this product may cause. | |
| * | |
| * Code change notes: | |
| * | |
| * Author Change Date | |
| * ****************************************************************************** | |
| * Jan Källman Initial Release 2009-10-01 | |
| * Jan Källman License changed GPL-->LGPL 2011-12-27 | |
| * Eyal Seagull Add "CreateComplexNode" 2012-04-03 | |
| * Eyal Seagull Add "DeleteTopNode" 2012-04-13 | |
| *******************************************************************************/ | |
| using System; | |
| using System.Collections.Immutable; | |
| using System.Globalization; | |
| using System.IO; | |
| using System.Text; | |
| using System.Xml; | |
| namespace AppsheetEpplus; | |
| /// <summary> | |
| /// Help class containing XML functions. | |
| /// Can be Inherited | |
| /// </summary> | |
| public abstract class XmlHelper { | |
| internal delegate int ChangedEventHandler(StyleBase sender, StyleChangeEventArgs e); | |
| internal XmlHelper(XmlNamespaceManager nameSpaceManager) { | |
| TopNode = null; | |
| NameSpaceManager = nameSpaceManager; | |
| } | |
| internal XmlHelper(XmlNamespaceManager nameSpaceManager, XmlNode topNode) { | |
| TopNode = topNode; | |
| NameSpaceManager = nameSpaceManager; | |
| } | |
| //internal bool ChangedFlag; | |
| internal XmlNamespaceManager NameSpaceManager { get; set; } | |
| internal XmlNode TopNode { get; set; } | |
| /// <summary> | |
| /// Schema order list | |
| /// </summary> | |
| protected virtual ImmutableArray<string> SchemaNodeOrder => ImmutableArray<string>.Empty; | |
| internal XmlNode CreateNode(string path) { | |
| if (path == "") { | |
| return TopNode; | |
| } | |
| return CreateNode(path, false); | |
| } | |
| internal XmlNode CreateNode(string path, bool insertFirst) { | |
| XmlNode node = TopNode; | |
| XmlNode prependNode = null; | |
| foreach (string subPath in path.Split('/')) { | |
| XmlNode subNode = node.SelectSingleNode(subPath, NameSpaceManager); | |
| if (subNode == null) { | |
| string nodeName; | |
| string nodePrefix; | |
| string nameSpaceUri; | |
| string[] nameSplit = subPath.Split(':'); | |
| if (SchemaNodeOrder.Length > 0 && subPath[0] != '@') { | |
| insertFirst = false; | |
| prependNode = GetPrependNode(subPath, node); | |
| } | |
| if (nameSplit.Length > 1) { | |
| nodePrefix = nameSplit[0]; | |
| if (nodePrefix[0] == '@') { | |
| nodePrefix = nodePrefix.Substring(1, nodePrefix.Length - 1); | |
| } | |
| nameSpaceUri = NameSpaceManager.LookupNamespace(nodePrefix); | |
| nodeName = nameSplit[1]; | |
| } else { | |
| nodePrefix = ""; | |
| nameSpaceUri = ""; | |
| nodeName = nameSplit[0]; | |
| } | |
| if (subPath.StartsWith("@")) { | |
| XmlAttribute addedAtt = node.OwnerDocument.CreateAttribute( | |
| subPath.Substring(1, subPath.Length - 1), | |
| nameSpaceUri); //nameSpaceURI | |
| node.Attributes.Append(addedAtt); | |
| } else { | |
| if (nodePrefix == "") { | |
| subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri); | |
| } else { | |
| if (nodePrefix == "" | |
| || (node.OwnerDocument != null | |
| && node.OwnerDocument.DocumentElement != null | |
| && node.OwnerDocument.DocumentElement.NamespaceURI == nameSpaceUri | |
| && node.OwnerDocument.DocumentElement.Prefix == "")) { | |
| subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri); | |
| } else { | |
| subNode = node.OwnerDocument.CreateElement(nodePrefix, nodeName, nameSpaceUri); | |
| } | |
| } | |
| if (prependNode != null) { | |
| node.InsertBefore(subNode, prependNode); | |
| prependNode = null; | |
| } else if (insertFirst) { | |
| node.PrependChild(subNode); | |
| } else { | |
| node.AppendChild(subNode); | |
| } | |
| } | |
| } | |
| node = subNode; | |
| } | |
| return node; | |
| } | |
| /// <summary> | |
| /// Options to insert a node in the XmlDocument | |
| /// </summary> | |
| internal enum eNodeInsertOrder { | |
| /// <summary> | |
| /// Insert as first node of "topNode" | |
| /// </summary> | |
| First, | |
| /// <summary> | |
| /// Insert as the last child of "topNode" | |
| /// </summary> | |
| Last, | |
| /// <summary> | |
| /// Insert after the "referenceNode" | |
| /// </summary> | |
| After, | |
| /// <summary> | |
| /// Insert before the "referenceNode" | |
| /// </summary> | |
| Before, | |
| /// <summary> | |
| /// Use the Schema List to insert in the right order. If the Schema list | |
| /// is null or empty, consider "Last" as the selected option | |
| /// </summary> | |
| SchemaOrder, | |
| } | |
| /// <summary> | |
| /// Create a complex node. Insert the node according to the <paramref name="path"/> | |
| /// using the <paramref name="topNode"/> as the parent | |
| /// </summary> | |
| /// <param name="topNode"></param> | |
| /// <param name="path"></param> | |
| /// <returns></returns> | |
| internal XmlNode CreateComplexNode(XmlNode topNode, string path) { | |
| return CreateComplexNode(topNode, path, eNodeInsertOrder.SchemaOrder, null); | |
| } | |
| /// <summary> | |
| /// Creates complex XML nodes | |
| /// </summary> | |
| /// <remarks> | |
| /// 1. "d:conditionalFormatting" | |
| /// 1.1. Creates/find the first "conditionalFormatting" node | |
| /// | |
| /// 2. "d:conditionalFormatting/@sqref" | |
| /// 2.1. Creates/find the first "conditionalFormatting" node | |
| /// 2.2. Creates (if not exists) the @sqref attribute | |
| /// | |
| /// 3. "d:conditionalFormatting/@id='7'/@sqref='A9:B99'" | |
| /// 3.1. Creates/find the first "conditionalFormatting" node | |
| /// 3.2. Creates/update its @id attribute to "7" | |
| /// 3.3. Creates/update its @sqref attribute to "A9:B99" | |
| /// | |
| /// 4. "d:conditionalFormatting[@id='7']/@sqref='X1:X5'" | |
| /// 4.1. Creates/find the first "conditionalFormatting" node with @id=7 | |
| /// 4.2. Creates/update its @sqref attribute to "X1:X5" | |
| /// | |
| /// 5. "d:conditionalFormatting[@id='7']/@id='8'/@sqref='X1:X5'/d:cfRule/@id='AB'" | |
| /// 5.1. Creates/find the first "conditionalFormatting" node with @id=7 | |
| /// 5.2. Set its @id attribute to "8" | |
| /// 5.2. Creates/update its @sqref attribute and set it to "X1:X5" | |
| /// 5.3. Creates/find the first "cfRule" node (inside the node) | |
| /// 5.4. Creates/update its @id attribute to "AB" | |
| /// | |
| /// 6. "d:cfRule/@id=''" | |
| /// 6.1. Creates/find the first "cfRule" node | |
| /// 6.1. Remove the @id attribute | |
| /// </remarks> | |
| /// <param name="topNode"></param> | |
| /// <param name="path"></param> | |
| /// <param name="nodeInsertOrder"></param> | |
| /// <param name="referenceNode"></param> | |
| /// <returns>The last node creates/found</returns> | |
| internal XmlNode CreateComplexNode( | |
| XmlNode topNode, | |
| string path, | |
| eNodeInsertOrder nodeInsertOrder, | |
| XmlNode referenceNode) { | |
| // Path is obrigatory | |
| if ((path == null) || (path == string.Empty)) { | |
| return topNode; | |
| } | |
| XmlNode node = topNode; | |
| //TODO: BUG: when the "path" contains "/" in an attrribue value, it gives an error. | |
| // Separate the XPath to Nodes and Attributes | |
| foreach (string subPath in path.Split('/')) { | |
| // The subPath can be any one of those: | |
| // nodeName | |
| // x:nodeName | |
| // nodeName[find criteria] | |
| // x:nodeName[find criteria] | |
| // @attribute | |
| // @attribute='attribute value' | |
| // Check if the subPath has at least one character | |
| if (subPath.Length > 0) { | |
| // Check if the subPath is an attribute (with or without value) | |
| if (subPath.StartsWith("@")) { | |
| // @attribute --> Create attribute | |
| // @attribute='' --> Remove attribute | |
| // @attribute='attribute value' --> Create attribute + update value | |
| string[] attributeSplit = subPath.Split('='); | |
| string attributeName = attributeSplit[0].Substring(1, attributeSplit[0].Length - 1); | |
| string attributeValue = null; // Null means no attribute value | |
| // Check if we have an attribute value to set | |
| if (attributeSplit.Length > 1) { | |
| // Remove the ' or " from the attribute value | |
| attributeValue = attributeSplit[1].Replace("'", "").Replace("\"", ""); | |
| } | |
| // Get the attribute (if exists) | |
| XmlAttribute attribute = (XmlAttribute)(node.Attributes.GetNamedItem(attributeName)); | |
| // Remove the attribute if value is empty (not null) | |
| if (attributeValue == string.Empty) { | |
| // Only if the attribute exists | |
| if (attribute != null) { | |
| node.Attributes.Remove(attribute); | |
| } | |
| } else { | |
| // Create the attribue if does not exists | |
| if (attribute == null) { | |
| // Create the attribute | |
| attribute = node.OwnerDocument.CreateAttribute(attributeName); | |
| // Add it to the current node | |
| node.Attributes.Append(attribute); | |
| } | |
| // Update the attribute value | |
| if (attributeValue != null) { | |
| node.Attributes[attributeName].Value = attributeValue; | |
| } | |
| } | |
| } else { | |
| // nodeName | |
| // x:nodeName | |
| // nodeName[find criteria] | |
| // x:nodeName[find criteria] | |
| // Look for the node (with or without filter criteria) | |
| XmlNode subNode = node.SelectSingleNode(subPath, NameSpaceManager); | |
| // Check if the node does not exists | |
| if (subNode == null) { | |
| string nodeName; | |
| string nodePrefix; | |
| string[] nameSplit = subPath.Split(':'); | |
| string nameSpaceUri; | |
| // Check if the name has a prefix like "d:nodeName" | |
| if (nameSplit.Length > 1) { | |
| nodePrefix = nameSplit[0]; | |
| nameSpaceUri = NameSpaceManager.LookupNamespace(nodePrefix); | |
| nodeName = nameSplit[1]; | |
| } else { | |
| nodePrefix = string.Empty; | |
| nameSpaceUri = string.Empty; | |
| nodeName = nameSplit[0]; | |
| } | |
| // Check if we have a criteria part in the node name | |
| if (nodeName.IndexOf("[") > 0) { | |
| // remove the criteria from the node name | |
| nodeName = nodeName.Substring(0, nodeName.IndexOf("[")); | |
| } | |
| if (nodePrefix == string.Empty) { | |
| subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri); | |
| } else { | |
| if (node.OwnerDocument != null | |
| && node.OwnerDocument.DocumentElement != null | |
| && node.OwnerDocument.DocumentElement.NamespaceURI == nameSpaceUri | |
| && node.OwnerDocument.DocumentElement.Prefix == string.Empty) { | |
| subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri); | |
| } else { | |
| subNode = node.OwnerDocument.CreateElement(nodePrefix, nodeName, nameSpaceUri); | |
| } | |
| } | |
| // Check if we need to use the "SchemaOrder" | |
| if (nodeInsertOrder == eNodeInsertOrder.SchemaOrder) { | |
| // Check if the Schema Order List is empty | |
| if (SchemaNodeOrder.Length == 0) { | |
| // Use the "Insert Last" option when Schema Order List is empty | |
| nodeInsertOrder = eNodeInsertOrder.Last; | |
| } else { | |
| // Find the prepend node in order to insert | |
| referenceNode = GetPrependNode(nodeName, node); | |
| if (referenceNode != null) { | |
| nodeInsertOrder = eNodeInsertOrder.Before; | |
| } else { | |
| nodeInsertOrder = eNodeInsertOrder.Last; | |
| } | |
| } | |
| } | |
| switch (nodeInsertOrder) { | |
| case eNodeInsertOrder.After: | |
| node.InsertAfter(subNode, referenceNode); | |
| referenceNode = null; | |
| break; | |
| case eNodeInsertOrder.Before: | |
| node.InsertBefore(subNode, referenceNode); | |
| referenceNode = null; | |
| break; | |
| case eNodeInsertOrder.First: | |
| node.PrependChild(subNode); | |
| break; | |
| case eNodeInsertOrder.Last: | |
| node.AppendChild(subNode); | |
| break; | |
| } | |
| } | |
| // Make the newly created node the top node when the rest of the path | |
| // is being evaluated. So newly created nodes will be the children of the | |
| // one we just created. | |
| node = subNode; | |
| } | |
| } | |
| } | |
| // Return the last created/found node | |
| return node; | |
| } | |
| /// <summary> | |
| /// return Prepend node | |
| /// </summary> | |
| /// <param name="nodeName">name of the node to check</param> | |
| /// <param name="node">Topnode to check children</param> | |
| /// <returns></returns> | |
| private XmlNode GetPrependNode(string nodeName, XmlNode node) { | |
| int pos = GetNodePos(nodeName); | |
| if (pos < 0) { | |
| return null; | |
| } | |
| XmlNode prependNode = null; | |
| foreach (XmlNode childNode in node.ChildNodes) { | |
| int childPos = GetNodePos(childNode.Name); | |
| if (childPos | |
| > -1) //Found? | |
| { | |
| if (childPos | |
| > pos) //Position is before | |
| { | |
| prependNode = childNode; | |
| break; | |
| } | |
| } | |
| } | |
| return prependNode; | |
| } | |
| private int GetNodePos(string nodeName) { | |
| int ix = nodeName.IndexOf(":"); | |
| if (ix > 0) { | |
| nodeName = nodeName.Substring(ix + 1, nodeName.Length - (ix + 1)); | |
| } | |
| for (int i = 0; i < SchemaNodeOrder.Length; i++) { | |
| if (nodeName == SchemaNodeOrder[i]) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| internal void DeleteAllNode(string path) { | |
| string[] split = path.Split('/'); | |
| XmlNode node = TopNode; | |
| foreach (string s in split) { | |
| node = node.SelectSingleNode(s, NameSpaceManager); | |
| if (node != null) { | |
| if (node is XmlAttribute attribute) { | |
| attribute.OwnerElement.Attributes.Remove(attribute); | |
| } else { | |
| node.ParentNode.RemoveChild(node); | |
| } | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| internal void DeleteNode(string path) { | |
| var node = TopNode.SelectSingleNode(path, NameSpaceManager); | |
| if (node != null) { | |
| if (node is XmlAttribute att) { | |
| att.OwnerElement.Attributes.Remove(att); | |
| } else { | |
| node.ParentNode.RemoveChild(node); | |
| } | |
| } | |
| } | |
| internal void SetXmlNodeString(string path, string value) { | |
| SetXmlNodeString(TopNode, path, value, false, false); | |
| } | |
| internal void SetXmlNodeString(string path, string value, bool removeIfBlank) { | |
| SetXmlNodeString(TopNode, path, value, removeIfBlank, false); | |
| } | |
| internal void SetXmlNodeString(XmlNode node, string path, string value, bool removeIfBlank) { | |
| SetXmlNodeString(node, path, value, removeIfBlank, false); | |
| } | |
| internal void SetXmlNodeString( | |
| XmlNode node, | |
| string path, | |
| string value, | |
| bool removeIfBlank, | |
| bool insertFirst) { | |
| if (node == null) { | |
| return; | |
| } | |
| if (value == "" && removeIfBlank) { | |
| DeleteAllNode(path); | |
| } else { | |
| XmlNode nameNode = node.SelectSingleNode(path, NameSpaceManager); | |
| if (nameNode == null) { | |
| CreateNode(path, insertFirst); | |
| nameNode = node.SelectSingleNode(path, NameSpaceManager); | |
| } | |
| //if (nameNode.InnerText != value) HasChanged(); | |
| nameNode.InnerText = value; | |
| } | |
| } | |
| internal void SetXmlNodeBool(string path, bool value) { | |
| SetXmlNodeString(TopNode, path, value ? "1" : "0", false, false); | |
| } | |
| internal void SetXmlNodeBool(string path, bool value, bool removeIf) { | |
| if (value == removeIf) { | |
| var node = TopNode.SelectSingleNode(path, NameSpaceManager); | |
| if (node != null) { | |
| if (node is XmlAttribute attribute) { | |
| var elem = attribute.OwnerElement; | |
| elem.ParentNode.RemoveChild(elem); | |
| } else { | |
| TopNode.RemoveChild(node); | |
| } | |
| } | |
| } else { | |
| SetXmlNodeString(TopNode, path, value ? "1" : "0", false, false); | |
| } | |
| } | |
| internal bool ExistNode(string path) { | |
| if (TopNode == null || TopNode.SelectSingleNode(path, NameSpaceManager) == null) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| internal bool? GetXmlNodeBoolNullable(string path) { | |
| var value = GetXmlNodeString(path); | |
| if (string.IsNullOrEmpty(value)) { | |
| return null; | |
| } | |
| return GetXmlNodeBool(path); | |
| } | |
| internal bool GetXmlNodeBool(string path) { | |
| return GetXmlNodeBool(path, false); | |
| } | |
| internal bool GetXmlNodeBool(string path, bool blankValue) { | |
| string value = GetXmlNodeString(path); | |
| if (value == "1" | |
| || value == "-1" | |
| || value.Equals("true", StringComparison.InvariantCultureIgnoreCase)) { | |
| return true; | |
| } | |
| if (value == "") { | |
| return blankValue; | |
| } | |
| return false; | |
| } | |
| internal int GetXmlNodeInt(string path) { | |
| if (int.TryParse(GetXmlNodeString(path), out var i)) { | |
| return i; | |
| } | |
| return int.MinValue; | |
| } | |
| internal int? GetXmlNodeIntNull(string path) { | |
| string s = GetXmlNodeString(path); | |
| if (s != "" && int.TryParse(s, out var i)) { | |
| return i; | |
| } | |
| return null; | |
| } | |
| internal decimal GetXmlNodeDecimal(string path) { | |
| if (decimal.TryParse( | |
| GetXmlNodeString(path), | |
| NumberStyles.Any, | |
| CultureInfo.InvariantCulture, | |
| out var d)) { | |
| return d; | |
| } | |
| return 0; | |
| } | |
| internal decimal? GetXmlNodeDecimalNull(string path) { | |
| if (decimal.TryParse( | |
| GetXmlNodeString(path), | |
| NumberStyles.Any, | |
| CultureInfo.InvariantCulture, | |
| out var d)) { | |
| return d; | |
| } | |
| return null; | |
| } | |
| internal double? GetXmlNodeDoubleNull(string path) { | |
| string s = GetXmlNodeString(path); | |
| if (s == "") { | |
| return null; | |
| } | |
| if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var v)) { | |
| return v; | |
| } | |
| return null; | |
| } | |
| internal double GetXmlNodeDouble(string path) { | |
| string s = GetXmlNodeString(path); | |
| if (s == "") { | |
| return double.NaN; | |
| } | |
| if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var v)) { | |
| return v; | |
| } | |
| return double.NaN; | |
| } | |
| internal string GetXmlNodeString(XmlNode node, string path) { | |
| if (node == null) { | |
| return ""; | |
| } | |
| XmlNode nameNode = node.SelectSingleNode(path, NameSpaceManager); | |
| if (nameNode != null) { | |
| if (nameNode.NodeType == XmlNodeType.Attribute) { | |
| return nameNode.Value ?? ""; | |
| } | |
| return nameNode.InnerText; | |
| } | |
| return ""; | |
| } | |
| internal string GetXmlNodeString(string path) { | |
| return GetXmlNodeString(TopNode, path); | |
| } | |
| internal static void LoadXmlSafe(XmlDocument xmlDoc, Stream stream) { | |
| XmlReaderSettings settings = new XmlReaderSettings(); | |
| //Disable entity parsing (to aviod xmlbombs, External Entity Attacks etc). | |
| settings.ProhibitDtd = true; | |
| XmlReader reader = XmlReader.Create(stream, settings); | |
| xmlDoc.Load(reader); | |
| } | |
| internal static void LoadXmlSafe(XmlDocument xmlDoc, string xml, Encoding encoding) { | |
| var stream = new MemoryStream(encoding.GetBytes(xml)); | |
| LoadXmlSafe(xmlDoc, stream); | |
| } | |
| } |