#region license // This file is part of Vocaluxe. // // Vocaluxe is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Vocaluxe 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Vocaluxe. If not, see . #endregion using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Xml; using VocaluxeLib.Log; namespace VocaluxeLib.Xml { public class CXmlException : Exception { #region Debug Helpers /// /// Returns the xPath from root to the node /// /// /// private string _GetXPath(XmlNode node) { var builder = new StringBuilder(); while (node != null) { switch (node.NodeType) { case XmlNodeType.Attribute: builder.Insert(0, "/@" + node.Name); node = ((XmlAttribute)node).OwnerElement; break; case XmlNodeType.Element: var index = _FindElementIndex(node); builder.Insert(0, "/" + node.Name + index); node = node.ParentNode; break; case XmlNodeType.Document: return builder.ToString(); default: throw new ArgumentException("Only elements and attributes are supported"); } } throw new ArgumentException("Node was not in a document"); } /// /// Returns an index as a string or empty string if the node name is sufficient to identify the node /// /// /// private string _FindElementIndex(XmlNode element) { var parentNode = element.ParentNode; if (parentNode is XmlDocument) { return ""; } var parent = (XmlElement)parentNode; var index = 1; if (parent != null) { var siblings = parent.ChildNodes.Cast().Where(candidate => candidate is XmlElement && candidate.Name == element.Name).ToArray(); foreach (var candidate in siblings) { if (candidate == element) { return siblings.Length > 1 ? "[" + index + "]" : ""; } index++; } } throw new ArgumentException("Couldn't find element within parent"); } #endregion Debug Helpers public readonly XmlNode Node; public readonly bool IsError; public CXmlException(string msg, XmlNode node, bool isError = true) : base(msg) { IsError = isError; Node = node; } public override string Message { get { return ToString(); } } public override String ToString() { var xPath = Node == null ? "" : _GetXPath(Node); var type = IsError ? "Error" : "Warning"; return type + ": " + base.Message.Replace("%n", xPath); } } public class CXmlMissingElementException : CXmlException { public readonly SFieldInfo Field; public CXmlMissingElementException(XmlNode parent, SFieldInfo field, bool isError = true) : base("Element: " + field.Name + " is missing in %n", parent, isError) { Field = field; } } public class CXmlInvalidValueException : CXmlException { private readonly string _Value; public CXmlInvalidValueException(string msg, XmlNode node, string value, bool isError = true) : base(msg, node, isError) { _Value = value; } public override String ToString() { return base.ToString().Replace("%v", _Value); } } public interface IXmlErrorHandler { /// /// Gets called for every error that occurs during xml reading. /// Recommended way of handling is throwing that error and log it
/// However if the function returns without throwing a fallback solution is applied to continue reading where possible e.g. by using default values or ignoring the element ///
/// Error that would be thrown void HandleError(CXmlException e); } /// /// Class for simple construction of error handlers by using a delegate /// public class CXmlErrorHandler : IXmlErrorHandler { public delegate void HandleErrorDelegate(CXmlException e); private readonly HandleErrorDelegate _HandleError; public CXmlErrorHandler(HandleErrorDelegate handleError) { _HandleError = handleError; } public void HandleError(CXmlException e) { _HandleError(e); } } public class CXmlDeserializer { public class CXmlDefaultErrorHandler : IXmlErrorHandler { public virtual void HandleError(CXmlException e) { if (e.IsError) { throw e; } CLog.Error(e.ToString()); } } private readonly IXmlErrorHandler _ErrorHandler; private readonly bool _UnescapeStringValues; /// /// Creates a new XmlDeserializer /// /// Set to true if string values should be unescaped during reading public CXmlDeserializer(bool unescapeStringValues) { _ErrorHandler = new CXmlDefaultErrorHandler(); _UnescapeStringValues = unescapeStringValues; } /// /// Creates a new XmlDeserializer /// /// Class to be used for error callbacks, if not given a default handler will be used which throws errors and logs warnings /// Set to true if string values should be unescaped during reading public CXmlDeserializer(IXmlErrorHandler errorHandler = null, bool unescapeStringValues = false) { _ErrorHandler = errorHandler ?? new CXmlDefaultErrorHandler(); _UnescapeStringValues = unescapeStringValues; } /// /// Returns the value if the given node /// /// Node to process /// Type of the object at that node /// Name of the sub nodes for array types(Array,List,Dictionary), can be set by XmlArrayItemAttribute and defaults to the type name /// If given, no new class is created and value is used instead, also used as default value /// private object _GetValue(XmlNode node, Type type, string subName = null, object value = null) { if (type.IsEnum) { if (node == null) { return null; } var stringValue = node.InnerText; try { return Enum.Parse(type, stringValue); } catch (Exception) { _ErrorHandler.HandleError(new CXmlInvalidValueException("Invalid value '%v' in %n", node, stringValue)); return value; } } if (type == typeof(Guid)) { return Guid.Parse(node.InnerText); } if (type == typeof(string)) { if (node == null) { return null; } else if (_UnescapeStringValues) { return Regex.Unescape(node.InnerText); } else { return node.InnerText; } } if (type.IsPrimitive) { return _GetPrimitiveValue(node, type) ?? value; } if (type.IsNullable()) { if (node == null || !node.HasChildNodes) { return null; } var subType = type.GetGenericArguments()[0]; return _GetValue(node, subType, subName, value); } if (type.IsList() || type.IsArray) { var subType = type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0]; if (subName == null) { subName = subType.GetTypeName(); } subName = subName.ToLowerInvariant(); var subValues = new List(); if (node != null) { foreach (XmlNode subNode in node.ChildNodes) { if (subNode is XmlComment) { continue; } if (subName == subNode.Name.ToLowerInvariant() && !subNode.Name.ToLowerInvariant().StartsWith(subName)) { _ErrorHandler.HandleError(new CXmlException("Invalid list entry '" + subNode.Name + "' in %n; Expected: " + subName, node)); } var subValue = _GetValue(subNode, subType); if (subValue != null) { subValues.Add(subValue); } } } if (value == null) { return _CreateList(type, subValues); } _FillList(value, type, subValues); return value; } if (type.IsDictionary()) { var subType = type.GetGenericArguments()[1]; var dict = value ?? Activator.CreateInstance(type); var add = type.GetMethod("Add"); if (subName != null) { subName = subName.ToLowerInvariant(); } if (node != null) { foreach (XmlNode subNode in node.ChildNodes) { if (subNode is XmlComment) { continue; } string key; if (subName != null) { if (subName != subNode.Name.ToLowerInvariant() && !subNode.Name.ToLowerInvariant().StartsWith(subName)) { _ErrorHandler.HandleError(new CXmlException("Invalid dictionary entry '" + subNode.Name + "' in %n; Expected: " + subName, node)); } var nameAtt = subNode.Attributes == null ? null : subNode.Attributes.GetNamedItem("name"); if (nameAtt == null) { _ErrorHandler.HandleError(new CXmlException("'name' attribute is missing in %n", subNode)); continue; } key = nameAtt.Value; } else { key = subNode.Name; } var subValue = _GetValue(subNode, subType); if (subValue != null) { add.Invoke(dict, new object[] { key, subValue }); } } } return dict; } if (value == null) { try { value = Activator.CreateInstance(type); } catch (Exception) { _ErrorHandler.HandleError(new CXmlException("Could not create instance of %n(Type=" + type.Name + ")", node)); return null; } } _ReadChildNodes(node, value, true); _ReadChildNodes(node, value, false); return value; } /// /// Gets the node's value assuming it is a primitive (int,float,...) /// /// /// /// private object _GetPrimitiveValue(XmlNode node, Type type) { if (node == null) { return null; } object value; var nodeVal = node.InnerText; try { var p = nodeVal.IndexOf(','); if (p > 0 && p >= nodeVal.Length - 3) { _ErrorHandler.HandleError(new CXmlInvalidValueException("German number format converted to English in %n", node, nodeVal, false)); var tmp = nodeVal.ToCharArray(); tmp[p] = '.'; nodeVal = new string(tmp); } value = Convert.ChangeType(nodeVal, type, CultureInfo.InvariantCulture); } catch (FormatException e) { _ErrorHandler.HandleError(new CXmlInvalidValueException("Invalid format in %n: '%v' (" + e.Message + ")", node, nodeVal)); return null; } catch (InvalidCastException e) { _ErrorHandler.HandleError(new CXmlInvalidValueException(e.Message + " in %n: '%v'", node, nodeVal)); return null; } return value; } /// /// Sets the field to its default value if it has one (specified as DefaultValueAttribute or an empty collection if it is an embedded one) /// /// /// /// True if default value was set private static bool _CheckAndSetDefaultValue(object result, SFieldInfo field) { if (field.IsEmbeddedList) { _AddList(result, field, new List()); return true; } if (!field.HasDefaultValue) { return false; } field.SetValue(result, field.DefaultValue); return true; } /// /// Fills the specified collection (array or list) with the values /// /// List to fill /// Type of the list /// Collection of values private static void _FillList(object list, Type type, ICollection values) { if (values.Count <= 0) { return; } if (type.IsArray) { var array = (Array)list; var i = 0; foreach (var value in values) { array.SetValue(value, i++); } } else { var addMethod = type.GetMethod("Add"); foreach (var value in values) { addMethod.Invoke(list, new object[] { value }); } } } /// /// Creates a collection (array or list) and fills it with the values /// /// Type of the collection to be created /// Collection of values /// Newly created collection private static object _CreateList(Type type, ICollection values) { var list = type.IsArray ? Array.CreateInstance(type.GetElementType(), values.Count) : Activator.CreateInstance(type, new object[] { values.Count }); _FillList(list, type, values); return list; } /// /// Creates a collection (array or list) and puts it into the specified field /// /// Object which listField belongs to /// Field that will contain the list /// Collection of values that are used to fill the list private static void _AddList(object o, SFieldInfo listField, ICollection values) { listField.SetValue(o, _CreateList(listField.Type, values)); } /// /// Reads childnodes of the given node (either nodes or attributes) /// /// Node to process /// Object to put the values in /// True for processing attributes, false for nodes private void _ReadChildNodes(XmlNode parent, object o, bool attributes) { IEnumerable nodes; if (parent == null) { nodes = null; } else if (attributes) { nodes = parent.Attributes; } else { nodes = parent.ChildNodes; } var fields = o.GetType().GetFields(attributes); if (nodes != null) { //Dictionary of all embedded lists to allow interleaved/mixed elements var embLists = new Dictionary>>(); foreach (XmlNode node in nodes) { if (node is XmlComment || node.LocalName == "xsd" || node.LocalName == "xsi") { continue; } var field = new SFieldInfo(); int curField; for (curField = 0; curField < fields.Count; curField++) { field = fields[curField]; if (field.Name == node.Name || field.AltName == node.Name || (field.IsEmbeddedList && node.Name.StartsWith(field.Name))) { break; } } if (curField >= fields.Count) { var msg = "Unexpected element: %n"; if (fields.Count > 0) { msg += " Expected: " + string.Join(", ", fields.Select(f => f.Name)); } _ErrorHandler.HandleError(new CXmlException(msg, node)); continue; } if (field.IsByteArray) { fields.RemoveAt(curField); //Do this also on error byte[] value; try { value = Convert.FromBase64String(node.InnerText); } catch (Exception) { _ErrorHandler.HandleError(new CXmlInvalidValueException("Invalid value in %n: %v", node, node.InnerText)); if (!_CheckAndSetDefaultValue(o, field)) { _ErrorHandler.HandleError(new CXmlException("No default value for %n", node)); } continue; } field.SetValue(o, value); } else if (field.IsEmbeddedList) { Tuple> entry; if (!embLists.TryGetValue(field.Name, out entry)) { entry = new Tuple>(field, new List()); embLists.Add(field.Name, entry); } var subValue = _GetValue(node, field.SubType); if (subValue != null) { entry.Item2.Add(subValue); } } else { fields.RemoveAt(curField); //Do this also on error var value = _GetValue(node, field.Type, field.ArrayItemName); if (value == null) { if (_CheckAndSetDefaultValue(o, field)) { continue; } if (!field.IsNullable) { _ErrorHandler.HandleError(new CXmlException("No default value for unset field at %n", node)); continue; } } if (field.Ranged != null && value != null && !field.Ranged.IsValid(field.IsNullable ? field.SubType : field.Type, value)) { _ErrorHandler.HandleError(new CXmlInvalidValueException("Value in %n is not in the range " + field.Ranged + ". (Value=%v)", node, value.ToString())); } field.SetValue(o, value); } } //Add embedded lists foreach (var entry in embLists.Values) { _AddList(o, entry.Item1, entry.Item2); fields.Remove(entry.Item1); } } foreach (var field in fields) { if (!_CheckAndSetDefaultValue(o, field)) { if (parent != null) { _ErrorHandler.HandleError(new CXmlMissingElementException(parent, field)); } var value = _GetValue(null, field.Type, field.ArrayItemName, field.GetValue(o)); if (value != null) { field.SetValue(o, value); } } } } /// /// Deserializes the content of read into the given object /// /// /// /// /// Deserialized object private T _Deserialize(XmlReader reader, object o) where T : new() { if (reader.IsEmptyElement) { _ErrorHandler.HandleError(new CXmlException("No content in xml!", null)); return (T)o; } var xDoc = new XmlDocument(); xDoc.Load(reader); if (xDoc.DocumentElement == null) { _ErrorHandler.HandleError(new CXmlException("Root element not found!", null)); return (T)o; } try { o = _GetValue(xDoc.DocumentElement, o.GetType(), o.GetType().GetSubTypeName(), o); } catch (TargetInvocationException e) { throw e.InnerException ?? e; } return (T)o; } /// /// Deserializes the given xml string and returns the resulting object /// /// Type of the object contained in the xml /// xml string /// New object of type T public T DeserializeString(string xml) where T : new() { return DeserializeString(xml, new T()); } /// /// Deserializes the given xml string and returns the resulting object
/// Does not create a new object but reuses the given one overwriting its values ///
/// Type of the object contained in the xml /// xml string /// Existing object /// New object of type T public T DeserializeString(string xml, T o) where T : new() { var reader = new XmlTextReader(new StringReader(xml)) { WhitespaceHandling = WhitespaceHandling.Significant, Normalization = true, XmlResolver = null }; try { return _Deserialize(reader, o); } finally { reader.Close(); } } /// /// Deserializes the given file and returns the resulting object /// /// Type of the object contained in the xml /// Full path to the file /// New object of type T public T Deserialize(string filePath) where T : new() { return Deserialize(filePath, new T()); } /// /// Deserializes the given file and returns the resulting object
/// Does not create a new object but reuses the given one overwriting its values ///
/// Type of the object contained in the xml /// Full path to the file /// Existing object /// New object of type T public T Deserialize(string filePath, T o) where T : new() { if (!File.Exists(filePath)) { throw new FileNotFoundException(filePath); } var reader = new XmlTextReader(filePath) { WhitespaceHandling = WhitespaceHandling.Significant, Normalization = true, XmlResolver = null }; try { return _Deserialize(reader, o); } finally { reader.Close(); } } } }