#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