/// Copyright 2016 Kopano b.v. /// /// This program is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License, version 3, /// as published by the Free Software Foundation. /// /// This program 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 Affero General Public License for more details. /// /// You should have received a copy of the GNU Affero General Public License /// along with this program.If not, see. /// /// Consult LICENSE file for details using Acacia.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml; namespace Acacia.ZPush.Connect.Soap { public class SoapSerializer { public static object Deserialize(XmlNode part, Type expectedType) { if (expectedType == null) throw new ArgumentException("expectedType is null"); return DeserializeNode(part, expectedType); } public static void Serialize(string name, object value, StringBuilder s) { if (value == null) { s.Append(string.Format("<{0} xsi:nil=\"true\"/>", name)); } else { // Type-specific parsing // Try simple types first TypeHandler handler = LookupType(value.GetType()); if (handler != null) { handler.Serialize(name, value, s); return; } // Check if serializable if (typeof(ISoapSerializable<>).IsGenericAssignableFrom(value.GetType())) { Type bound = typeof(ISoapSerializable<>).MakeGenericType(value.GetType().GetGenericArguments(typeof(ISoapSerializable<>))); MethodInfo method = bound.GetMethod("SoapSerialize"); object serializable = method.Invoke(value, new object[0]); Serialize(name, serializable, s); return; } // Enums if (value.GetType().IsEnum) { Serialize(name, (int)value, s); return; } // Serialize as structure TYPE_HANDLER_OBJECT.Serialize(name, value, s); } } private static object DeserializeNode(XmlNode part, Type expectedType) { // Check for null if (part.Attributes["nil", SoapConstants.XMLNS_XSI] != null) return null; // Type-specific parsing TypeHandler type = LookupType(part, expectedType); return type.Deserialize(part, expectedType); } #region Builtin types private class ConversionException : Exception { private readonly List trace = new List(); public ConversionException(XmlNode node, string message, Exception innerException) : base(message, innerException) { AddNode(node); } public void AddNode(XmlNode node) { trace.Add(node); } public override string Message { get { string context = ""; foreach(XmlNode node in trace) { int index = -1; int count = 0; if (node.ParentNode != null) { for (int i = 0; i < node.ParentNode.ChildNodes.Count; ++i) { if (node.ParentNode.ChildNodes[i].Name == node.Name) ++count; if (node.ParentNode.ChildNodes[i] == node) index = i; } } string suffix = (index < 0 || count <= 1) ? "" : ("[" + index + "]"); context = "/" + node.Name + suffix + context; } return base.Message + " @ " + context + "\n"; } } } private abstract class TypeHandler { public string FullName { get { return _xmlns + ":" + _name; } } public Type HandlesType { get { return _baseType; } } private readonly string _xmlns; private readonly string _name; private readonly Type _baseType; protected TypeHandler(string xmlns, string name, Type baseType) { this._xmlns = xmlns; this._name = name; this._baseType = baseType; } public object Deserialize(XmlNode node, Type expectedType) { object value = null; try { value = DeserializeContents(node, expectedType); if (expectedType != null) { // Try to convert it to the expected type return SoapConvert(expectedType, value); } return value; } catch(ConversionException e) { e.AddNode(node); throw e; } catch(Exception e) { throw new ConversionException(node, string.Format("Cannot convert value '{0}' to {1}", value ?? node.InnerXml, expectedType ), e ); } } protected object SoapConvert(Type type, object value) { // Check if any conversion is needed if (value != null && type.IsAssignableFrom(value.GetType())) return value; if (value != null) { // Try Soap conversion if (typeof(ISoapSerializable<>).IsGenericAssignableFrom(type)) { // Get the serialization type Type serializationType = type.GetGenericArguments(typeof(ISoapSerializable<>))[0]; if (serializationType.IsAssignableFrom(value.GetType())) { // Create the instance return Activator.CreateInstance(type, value); } } } // Or standard conversions return type.Convert(value); } abstract protected object DeserializeContents(XmlNode node, Type expectedType); abstract public void Serialize(string name, object value, StringBuilder s); } #region Primitives private class TypeHandlerBoolean : TypeHandler { public TypeHandlerBoolean() : base(SoapConstants.XMLNS_XSD, "boolean", typeof(bool)) { } public override void Serialize(string name, object value, StringBuilder s) { s.Append(string.Format("<{0} xsi:type=\"xsd:boolean\">{1}", name, value)); } protected override object DeserializeContents(XmlNode node, Type expectedType) { return node.InnerText.ToLower().Equals("true"); } } abstract private class TypeHandlerIntegral : TypeHandler { public TypeHandlerIntegral(string xmlns, string name, Type baseType) : base(xmlns, name, baseType) { } public override void Serialize(string name, object value, StringBuilder s) { s.Append(string.Format("<{0} xsi:type=\"xsd:int\">{1}", name, value)); } protected override object DeserializeContents(XmlNode node, Type expectedType) { long value = long.Parse(node.InnerText); if (expectedType == typeof(DateTime)) { // TODO: make a util function for this? DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); return origin.AddSeconds(value); } return value; } } private class TypeHandlerInt : TypeHandlerIntegral { public TypeHandlerInt() : base(SoapConstants.XMLNS_XSD, null, typeof(int)) { } } private class TypeHandlerLong : TypeHandlerIntegral { public TypeHandlerLong() : base(SoapConstants.XMLNS_XSD, "int", typeof(long)) { } } abstract private class TypeHandlerReal : TypeHandler { public TypeHandlerReal(string xmlns, string name, Type baseType) : base(xmlns, name, baseType) { } public override void Serialize(string name, object value, StringBuilder s) { s.Append(string.Format("<{0} xsi:type=\"xsd:{2}\">{1}", name, value, HandlesType == typeof(float) ? "float" : "double")); } protected override object DeserializeContents(XmlNode node, Type expectedType) { double value = double.Parse(node.InnerText); if (expectedType == typeof(float)) return (float)value; return value; } } private class TypeHandlerFloat : TypeHandlerReal { public TypeHandlerFloat() : base(SoapConstants.XMLNS_XSD, "float", typeof(float)) { } } private class TypeHandlerDouble : TypeHandlerReal { public TypeHandlerDouble() : base(SoapConstants.XMLNS_XSD, "double", typeof(double)) { } } private class TypeHandlerString : TypeHandler { public TypeHandlerString() : base(SoapConstants.XMLNS_XSD, "string", typeof(string)) { } public override void Serialize(string name, object value, StringBuilder s) { s.Append(string.Format("<{0} xsi:type=\"xsd:string\">{1}", name, value.ToString().EncodeXML())); } protected override object DeserializeContents(XmlNode node, Type expectedType) { return node.InnerText; } } #endregion #region List private class TypeHandlerList : TypeHandler { public TypeHandlerList() : base(SoapConstants.XMLNS_SOAP_ENC, "Array", typeof(System.Collections.ICollection)) { } protected override object DeserializeContents(XmlNode node, Type expectedType) { if (expectedType == null) return null; if (expectedType.IsArray) { // Decode into array Array array = Array.CreateInstance(expectedType.GetElementType(), node.ChildNodes.Count); int index = 0; foreach (XmlNode child in node.ChildNodes) { object element = DeserializeNode(child, expectedType.GetElementType()); array.SetValue(element, index); ++index; } return array; } else { // Decode into list System.Collections.IList list = (System.Collections.IList)Activator.CreateInstance(expectedType); Type elementType = expectedType.GetGenericArguments()[0]; foreach (XmlNode child in node.ChildNodes) { object element = DeserializeNode(child, elementType); list.Add(element); } return list; } } public override void Serialize(string name, object value, StringBuilder s) { System.Collections.ICollection list = (System.Collections.ICollection)value; s.Append(string.Format("<{0} xsi:type=\"soap-enc:Array\" soap-enc:arrayType=\"ns2:Map[{1}]\">\n", name, list.Count)); foreach (object element in list) { SoapSerializer.Serialize("item", element, s); } s.Append(string.Format("\n", name)); } } #endregion #region Objects private abstract class TypeHandlerObject : TypeHandler { public TypeHandlerObject(string xmlns, string name) : base(xmlns, name, null) { } protected override object DeserializeContents(XmlNode node, Type expectedType) { if (expectedType == null) return null; // Determine if the expected type is an ISoapSerializable if (!typeof(ISoapSerializable<>).IsGenericAssignableFrom(expectedType)) { // Nope, try simple assignment return DeserializeContentsRaw(node, expectedType); } // Get the serialization type Type serializationType = expectedType.GetGenericArguments(typeof(ISoapSerializable<>))[0]; // Get the values as a dictionary Dictionary values = new Dictionary(); DeserializeMembers(node, serializationType, values); // Create the object return CreateCustomInstance(values, serializationType, expectedType); } private object DeserializeContentsRaw(XmlNode node, Type expectedType) { // TODO: better error on failure // Get the values as a dictionary Dictionary values = new Dictionary(); DeserializeMembers(node, expectedType, values); // And assign them to a new instance return DeserializeContentsRaw(values, expectedType); } private object DeserializeContentsRaw(Dictionary node, Type serializationType) { object instance = Activator.CreateInstance(serializationType); foreach (FieldInfo field in serializationType.GetFields()) { string key = field.GetCustomAttribute()?.FieldId?.ToString() ?? field.Name; object value = null; if (node.TryGetValue(key, out value)) { value = SoapConvert(field.FieldType, value); field.SetValue(instance, value); } } return instance; } abstract protected void DeserializeMembers(XmlNode node, Type serializationType, Dictionary values); private object CreateCustomInstance(Dictionary node, Type serializationType, Type finalType) { object instance; if (serializationType == typeof(Dictionary)) { instance = node; } else { // Initialise the serialization type instance = DeserializeContentsRaw(node, serializationType); } // Return the final type return Activator.CreateInstance(finalType, instance); } public override void Serialize(string name, object value, StringBuilder s) { Dictionary dict; if (typeof(Dictionary).IsAssignableFrom(value.GetType())) { dict = (Dictionary)value; } else { // Convert toe dictionary dict = new Dictionary(); foreach (FieldInfo field in value.GetType().GetFields()) { object fieldValue = field.GetValue(value); dict.Add(field.Name, fieldValue); } } // Encode SerializeMembers(name, dict, s); } protected abstract void SerializeMembers(string name, Dictionary fields, StringBuilder s); protected virtual Type DetermineChildType(Type type, string field) { if (type == null) return null; // Check if the field is a simple field name FieldInfo prop = type.GetField(field); if (prop == null && type.GetCustomAttribute() != null) // Check for an attriubte prop = type.GetFields().FirstOrDefault(f => f.GetCustomAttribute()?.FieldId?.ToString() == field); return prop?.FieldType; } } private class TypeHandlerStruct : TypeHandlerObject { public TypeHandlerStruct() : base(SoapConstants.XMLNS_SOAP_ENC, "Struct") { } protected override void DeserializeMembers(XmlNode node, Type expectedType, Dictionary dict) { foreach (XmlNode child in node.ChildNodes) { string key = child.Name; object value = DeserializeNode(child, DetermineChildType(expectedType, key)); dict.Add(key, value); } } protected override void SerializeMembers(string name, Dictionary fields, StringBuilder s) { throw new NotImplementedException(); } } private class TypeHandlerObjectMap : TypeHandlerObject { public TypeHandlerObjectMap() : base(SoapConstants.XMLNS_APACHE, "Map") { } protected override void DeserializeMembers(XmlNode node, Type expectedType, Dictionary dict) { foreach (XmlNode child in node.ChildNodes) { string key = (string)DeserializeNode(child.SelectSingleNode("key"), typeof(string)); Type childType = DetermineChildType(expectedType, key); object value = DeserializeNode(child.SelectSingleNode("value"), childType); dict.Add(key, value); } } protected override void SerializeMembers(string name, Dictionary fields, StringBuilder s) { s.Append(string.Format("<{0} xsi:type=\"ns2:Map\">\n", name)); foreach (var entry in fields) { s.Append("").Append(entry.Key).Append(""); SoapSerializer.Serialize("value", entry.Value, s); s.Append("\n"); } s.Append(string.Format("\n", name)); } } #endregion #region Map private class TypeHandlerMap : TypeHandler { public TypeHandlerMap() : base(SoapConstants.XMLNS_SOAP_ENC, "Array", typeof(System.Collections.ICollection)) { } protected override object DeserializeContents(XmlNode node, Type expectedType) { Dictionary map = new Dictionary(); foreach (XmlNode child in node.ChildNodes) { KeyType key = (KeyType)DeserializeNode(child.SelectSingleNode("key"), typeof(KeyType)); ValueType value = (ValueType)DeserializeNode(child.SelectSingleNode("value"), typeof(ValueType)); map.Add(key, value); } return map; } public override void Serialize(string name, object value, StringBuilder s) { throw new NotImplementedException(); } } #endregion private readonly static Dictionary TYPES_BY_FULL_NAME = new Dictionary(); private readonly static Dictionary TYPES_BY_TYPE = new Dictionary(); private readonly static TypeHandler TYPE_HANDLER_OBJECT = new TypeHandlerObjectMap(); static SoapSerializer() { RegisterTypeHandler(new TypeHandlerBoolean()); // What is called an int in soap might actually be a long here. RegisterTypeHandler(new TypeHandlerLong()); RegisterTypeHandler(new TypeHandlerInt()); RegisterTypeHandler(new TypeHandlerFloat()); RegisterTypeHandler(new TypeHandlerDouble()); RegisterTypeHandler(new TypeHandlerString()); RegisterTypeHandler(new TypeHandlerList()); RegisterTypeHandler(new TypeHandlerStruct()); RegisterTypeHandler(TYPE_HANDLER_OBJECT); } private static void RegisterTypeHandler(TypeHandler type) { TYPES_BY_FULL_NAME.Add(type.FullName, type); if (type.HandlesType != null) TYPES_BY_TYPE.Add(type.HandlesType, type); } private static TypeHandler LookupType(Type type) { TypeHandler handler; // Check exact type first if (TYPES_BY_TYPE.TryGetValue(type, out handler)) return handler; // Try subtypes foreach (KeyValuePair entry in TYPES_BY_TYPE) if (entry.Key.IsAssignableFrom(type)) return entry.Value; return null; } private static TypeHandler LookupType(XmlNode part, Type expectedType) { if (expectedType != null && typeof(IDictionary<,>).IsGenericAssignableFrom(expectedType)) { Type bound = typeof(TypeHandlerMap<,>).MakeGenericType(expectedType.GetGenericArguments(typeof(IDictionary<,>))); return (TypeHandler)Activator.CreateInstance(bound); } XmlAttribute typeAttr = part.Attributes["type", SoapConstants.XMLNS_XSI]; if (typeAttr == null) throw new Exception("Missing type: " + part.OuterXml); string value = typeAttr.Value; string[] parts = value.Split(new char[] { ':' }, 2); string fullName; if (parts.Length == 1) fullName = parts[0]; else fullName = part.GetNamespaceOfPrefix(parts[0]) + ":" + parts[1]; TypeHandler type; if (!TYPES_BY_FULL_NAME.TryGetValue(fullName, out type)) throw new Exception("Unknown type: " + fullName); return type; } #endregion } }