// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Reflection;
using System.Globalization;
using System.Xml.Serialization;
using System.ComponentModel;
using System.Collections;
using System.Xml;
namespace OpenTap.Plugins
{
///
/// Implemented by serializer plugins that creates and populates members of an object.
///
public interface IConstructingSerializer
{
/// The object currently being serialized/deserialized.
object Object { get; }
/// Optionally set to indicate which member of Object is being serialized/deserialized.
IMemberData CurrentMember { get; }
}
///
/// Default object serializer.
///
internal class ObjectSerializer : TapSerializerPlugin, ITapSerializerPlugin, IConstructingSerializer
{
///
/// Gets the member currently being serialized.
///
public IMemberData CurrentMember { get; private set; }
public static XName IgnoreMemberXName = "ignore-member";
///
/// Specifies order. Minimum order should be -1 as this is the most basic serializer.
///
public override double Order
{
get { return -1; }
}
/// The currently serializing or deserializing object.
public object Object { get; private set; }
Dictionary serializableMembers = new Dictionary();
///
/// Tries to deserialize an object from an XElement.
///
///
///
///
///
/// Whether warning messages should be emitted in case of missing properties.
/// True on success.
public virtual bool TryDeserializeObject(XElement element, ITypeData t, Action setter, object newobj = null, bool logWarnings = true)
{
if (element.IsEmpty && !element.HasAttributes)
{
setter(null);
return true;
}
if (newobj == null)
{
if (t.CanCreateInstance == false)
{
// if the instance type cannot be constructed,
// use the instance already on the object.
var objectSerializer = Serializer.SerializerStack.OfType().FirstOrDefault();
var ownerMember = objectSerializer?.CurrentMember;
var ownerObj = objectSerializer?.Object;
if (ownerMember == null || ownerObj == null)
{
throw new Exception($"Cannot create instance of {t} and no default value exists.");
}
if (ownerMember.HasAttribute())
{
// Use element factory if the member type is not the target type, but rather 'has a' target type.
// we dont need to detect if it 'has a' target type, lets just assume it.
if (ownerMember.TypeDescriptor.DescendsTo(t) && ownerMember.GetAttribute() is FactoryAttribute f)
{
newobj = FactoryAttribute.Create(ownerObj, f);
}
else if (ownerMember.GetAttribute() is ElementFactoryAttribute f2)
{
newobj = FactoryAttribute.Create(ownerObj, f2);
}
}
if (newobj == null)
{
newobj = ownerMember.GetValue(ownerObj);
if (newobj == null)
throw new Exception($"Unable to get default value of {ownerMember}");
}
}
else
{
newobj = t.CreateInstance(Array.Empty());
}
}
var prevobj = Object;
Object = newobj;
if (newobj == null)
throw new ArgumentNullException(nameof(newobj));
t = TypeData.GetTypeData(newobj);
var t2 = t;
var properties = serializableMembers.GetOrCreateValue(t2, t3 => t3.GetMembers()
.Where(x => x.HasAttribute() == false)
.ToArray());
try
{
foreach (var prop in properties)
{
var attr = prop.GetAttribute();
if (attr == null) continue;
var name = string.IsNullOrWhiteSpace(attr.AttributeName) ? prop.Name : attr.AttributeName;
var attr_value = element.Attribute(Serializer.PropertyXmlName(name));
if (attr_value != null)
{
try
{
var typeData = prop.TypeDescriptor.AsTypeData();
readContentInternal(typeData.Type, false, () => attr_value.Value, element, out object value);
prop.SetValue(newobj, value);
}
catch (Exception e)
{
if (logWarnings)
{
Log.Warning(element, "Attribute value '{0}' was not read correctly as a {1}", attr_value.Value, prop);
Log.Debug(e);
}
}
}
}
if (properties.FirstOrDefault(x => x.HasAttribute()) is IMemberData mem2)
{
object value;
if (mem2.TypeDescriptor is TypeData td &&
readContentInternal(td.Load(), false, () => element.Value, element, out object _value))
{ value = _value; }
else
value = StringConvertProvider.FromString(element.Value, mem2.TypeDescriptor, null);
mem2.SetValue(newobj, value);
}
else
{
var props = properties.ToLookup(x => x.GetAttributes().FirstOrDefault()?.ElementName ?? x.Name);
var elements = element.Elements().ToArray();
bool[] visited = new bool[elements.Length];
double order = 0;
int foundWithCurrentType = 0;
while (true)
{
double nextOrder = 1000;
// since the object might be dynamically adding properties as other props are added.
// we need to iterate a bit. Example: Test Plan Reference.
int found = visited.Count(x => x);
for (int i = 0; i < elements.Length; i++)
{
var element2 = elements[i];
if (visited[i]) continue;
if (element2.Attribute(IgnoreMemberXName) is XAttribute attr && attr.Value == "true")
{
visited[i] = true;
continue;
}
IMemberData property = null;
var name = XmlConvert.DecodeName(element2.Name.LocalName);
var propertyMatches = props[name];
int hits = 0;
foreach (var p in propertyMatches)
{
if (p.Writable || p.HasAttribute())
{
property = p;
hits++;
}
}
if (property == null)
{
continue; // later we might try this property again.
}
if (hits > 1)
Log.Warning(element2, "Multiple properties named '{0}' are available to the serializer in '{1}' this might give issues in serialization.", element2.Name.LocalName, t.GetAttribute().Name);
if (property.GetAttribute() is DeserializeOrderAttribute orderAttr)
{
if (order < orderAttr.Order)
{
if (orderAttr.Order < nextOrder)
{
nextOrder = orderAttr.Order;
}
continue;
}
}
else
{
nextOrder = order;
}
visited[i] = true;
var prev = CurrentMember;
CurrentMember = property;
try
{
if (CurrentMember.HasAttribute()) // This property shouldn't have been in the file in the first place, but in case it is (because of a change or a bug), we shouldn't try to set it. (E.g. SweepLoopRange.SweepStep)
if (!CurrentMember.HasAttribute()) // In the special case we assume that this is some compatibility property that still needs to be set if present in the XML. (E.g. TestPlanReference.DynamicDataContents)
continue;
if (property is MemberData mem && mem.Member is PropertyInfo Property && Property.PropertyType.HasInterface() && Property.PropertyType.IsGenericType && Property.HasAttribute())
{
// Special case to mimic old .NET XmlSerializer behavior
var list = (IList)Property.GetValue(newobj);
Action setValue = x => list.Add(x);
Serializer.Deserialize(element2, setValue, Property.PropertyType.GetGenericArguments().First());
}
else
{
void setValue(object x)
{
property.SetValue(newobj, x);
}
if (property.HasAttribute())
{
var current = property.GetValue(newobj);
if (current == null)
throw new Exception($"Unable to deserialize {property} in-place.");
this.TryDeserializeObject(element2, TypeData.GetTypeData(current), (x) => { },
current, true);
}
else
{
Serializer.Deserialize(element2, setValue, property.TypeDescriptor);
}
}
}
catch (Exception e)
{
if (logWarnings)
{
Log.Warning(element2, "Unable to set property '{0}'.", CurrentMember.Name);
Log.Warning("Error was: \"{0}\".", e.Message);
Log.Debug(e);
}
}
finally
{
CurrentMember = prev;
}
}
int nowFound = visited.Count(x => x);
if (found == nowFound && order == nextOrder)
{
// The might have changed by loading properties.
var t3 = TypeData.GetTypeData(newobj);
if (Equals(t3, t2) == false)
{
if (nowFound != foundWithCurrentType)
{ // check avoids infinite loop if ITypeData did not overload Equals.
foundWithCurrentType = nowFound;
t2 = t3;
continue;
}
}
if (nowFound < elements.Length)
{ // still elements left to check. Last resort to try loading at defer.
// Some of the items might not be deserializable before defer load.
// if this is the case do this as a defer action.
void postDeserialize()
{
t2 = TypeData.GetTypeData(newobj);
for(int j = 0; j < elements.Length; j++)
{
if (visited[j]) continue;
var propertyElement = elements[j];
IMemberData property = null;
var elementName = XmlConvert.DecodeName(propertyElement.Name.LocalName);
try
{
property = t2.GetMember(elementName);
if (property == null)
property = t2.GetMembers().FirstOrDefault(x => x.Name == elementName);
}
catch { }
if (property == null || property.Writable == false)
{
continue;
}
Action setValue = x =>
{
// Defer: this fixes an issue when `property` is ParameterMemberData,
// and the object being set has properties which are also deferred.
// This must be deferred because ParameterMemberData relies on IObjectCloner
// to clone the member value, but if this setter is not deferred, the objects
// will have been cloned before they are fully serialized
var priority = property is ParameterMemberData
? TapSerializer.DeferredLoadOrder.ParameterMemberDataSetter
: TapSerializer.DeferredLoadOrder.Normal;
Serializer.DeferLoad(() => property.SetValue(newobj, x), priority);
};
var prevobj2 = Object;
var prevmember = this.CurrentMember;
Object = newobj;
CurrentMember = property;
// at this point the serializer stack is technically empty,
// so add the current on top and deserialize.
Serializer.PushActiveSerializer(this);
try
{
Serializer.Deserialize(propertyElement, setValue, property.TypeDescriptor);
}
finally
{
// clean up.
Serializer.PopActiveSerializer();
Object = prevobj2;
CurrentMember = prevmember;
}
visited[j] = true;
}
// print a warning message if the element could not be deserialized.
for (int j = 0; j < elements.Length; j++)
{
if (visited[j]) continue;
var elem = elements[j];
var elementName = elem.Name.LocalName;
if (elementName.Contains('.') == false)
{
// if the element name contains '.' it is usually a special name and hence
// an error message is not needed. e.g:
// Package.Dependencies
// TestStep.Inputs
var message =
$"Unable to read element '{elem.Name.LocalName}'. The property does not exist.";
Serializer.PushError(elem, message);
}
}
}
Serializer.DeferLoad(postDeserialize);
}
break;
}
order = nextOrder;
}
}
setter(newobj);
}
finally
{
Object = prevobj;
if (newobj is IDeserializedCallback)
{
// Callback should be added in the end because an inner part of the object might also have defered it.
Serializer.DeferLoad(() =>
{
try
{
((IDeserializedCallback)newobj).OnDeserialized();
}
catch (Exception e)
{
Log.Warning("Exception caught while handling OnSerialized");
Log.Debug(e);
}
});
}
}
return true;
}
bool readContentInternal(Type propType, bool ignoreComponentSettings, Func getvalueString, XElement elem, out object outvalue)
{
object value = null;
bool ok;
if (propType.IsEnum || propType.IsPrimitive || propType == typeof(string) || propType.IsValueType || propType == typeof(Type))
{
ok = true;
string valueString = getvalueString()?.Trim();
if (propType.IsEnum)
{
if (!string.IsNullOrEmpty(valueString))
{
// legacy support: A flagged enum did not have ','s, but just spaces between.
if (!valueString.Contains(','))
{
var splitted = valueString.Split(' ');
value = Enum.Parse(propType, string.Join(",", splitted));
}
else
value = Enum.Parse(propType, valueString);
}
}
else if (propType == typeof(String) || propType == typeof(char))
{
if (elem.HasElements)
{
// string contains Base64 if it has invalid XML chars.
// elem.Element("Base64") fails if the attribute "xmlns" is set on the document.
// In this case, "Base64" must be prepended with the value of xmlns in brackets.
// If the namespace is not set, ns.GetName("Base64") will just evaluate to "Base64"
var ns = elem.GetDefaultNamespace();
var encode = elem.Element(ns.GetName("Base64"));
if (encode != null)
{
try
{
value = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encode.Value));
}
catch
{
value = encode.Value;
}
}
}
if (value == null)
value = valueString;
if (propType == typeof(char))
value = ((string) value)[0];
}
else if (propType == typeof(TimeSpan) || propType == typeof(TimeSpan?))
{
value = TimeSpan.Parse(valueString, CultureInfo.InvariantCulture);
}
else if (propType == typeof(Guid) || propType == typeof(Guid?))
{
value = Guid.Parse(valueString);
}
else if (propType == typeof(Type))
{
value = PluginManager.LocateType(valueString.Split(',').First());
}
else if (tryConvertNumber(propType, valueString, out value))
{
// Conversions done in tryConvertNumber
}
else if (propType.HasInterface())
{
value = Convert.ChangeType(valueString, propType, CultureInfo.InvariantCulture);
}else if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// For nullable we recurse focusing on the underlying non-nullable type.
if (valueString == null)
{
outvalue = null;
return true; // null is a valid value for nullable types.
}
return readContentInternal(Nullable.GetUnderlyingType(propType), ignoreComponentSettings, getvalueString, elem, out outvalue);
}
else
{
ok = false;
}
}
else
{
ok = false;
}
outvalue = value;
return ok;
}
static bool tryConvertNumber(Type type, string valueString, out object value)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.Byte:
{
byte oval;
bool ok = byte.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.SByte:
{
SByte oval;
bool ok = SByte.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.UInt16:
{
UInt16 oval;
bool ok = UInt16.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.UInt32:
{
UInt32 oval;
bool ok = UInt32.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.UInt64:
{
UInt64 oval;
bool ok = UInt64.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.Int16:
{
Int16 oval;
bool ok = Int16.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.Int32:
{
Int32 oval;
bool ok = Int32.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.Int64:
{
Int64 oval;
bool ok = Int64.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.Decimal:
{
value = BigFloat.Convert(valueString, CultureInfo.InvariantCulture).ConvertTo(typeof(decimal));
return true;
}
case TypeCode.Double:
{
Double oval;
bool ok = Double.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
case TypeCode.Single:
{
Single oval;
bool ok = Single.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
value = oval;
return ok;
}
default:
value = null;
return false;
}
}
// for detecting cycles in object serialization.
HashSet cycleDetetionSet = new HashSet();
bool AlwaysG17DoubleFormat = false;
internal static readonly XName DefaultValue = "DefaultValue";
///
/// Deserializes an object from XML.
///
///
///
///
///
public override bool Deserialize(XElement element, ITypeData t, Action setter)
{
try
{
if (t is TypeData ctd)
{
if (readContentInternal(ctd.Type, false, () => element.IsEmpty ? null : element.Value, element, out object obj))
{
setter(obj);
return true;
}
}
if (TryDeserializeObject(element, t, setter))
return true;
}
catch (Exception ex)
{
Serializer.HandleError(element, $"Unable to read {t.GetDisplayAttribute().GetFullName()}.", ex);
}
return false;
}
///
/// Checks if the string can be turned into an XML string.
/// This should return true only if the transformation to/from xml is reversible.
///
///
///
static bool containsOnlyReversibleTapXmlChars(string str)
{
int len = str.Length;
for(int i = 0; i < len; i++)
{
char c = str[i];
if (c == '\r') // special case. Somehow deserialization turns \n into \r.
return false;
if (char.IsLetter(c))
continue;
if (XmlConvert.IsXmlChar(c))
continue;
if(i < len - 1)
{
char c2 = str[i + 1];
if(XmlConvert.IsXmlSurrogatePair(c2, c))
continue;
}
return false;
}
return true;
}
private List GetUsedFiles(object obj)
{
var result = new List();
string GetStringOrMacroString(IMemberData prop, object o)
{
if (prop.HasAttribute())
return null;
object val = prop.GetValue(o);
if (val is string s)
return s;
if (val is MacroString m)
return m.ToString();
return null;
}
var td = TypeData.GetTypeData(obj);
var props = td.GetMembers().Where(m => m.HasAttribute());
foreach (var prop in props)
{
var path = GetStringOrMacroString(prop, obj);
if (string.IsNullOrWhiteSpace(path) == false)
result.Add(path);
}
return result;
}
// this 'cache' only caches serialized values during this instance of the serialize
// this is to avoid a memory leak for e.g GUIDs, which falls into the category of primitives.
readonly Dictionary enumTable = new Dictionary();
///
/// Serializes an object to XML.
///
///
///
///
///
public override bool Serialize(XElement elem, object obj, ITypeData expectedType)
{
if (obj == null)
return true;
object prevObj = Object;
// If cycleDetectorLut already contains an element, we've been here before.
// Note the cycleDetectorLut adds and removes the obj later if it is not already in it.
if (!cycleDetetionSet.Add(obj))
throw new Exception("Cycle detected");
foreach (var fileResource in GetUsedFiles(obj))
Serializer.NotifyFileUsed(fileResource);
object obj2 = obj;
try
{
Object = obj;
switch (obj)
{
case double d:
if (AlwaysG17DoubleFormat)
{
// the fastest and most accurate string representation of a double.
elem.Value = d.ToString("G17", CultureInfo.InvariantCulture);
return true;
}
else
{
// It was decided to use R instead of G17 for readability, although G17 is slightly faster.
// however, there is a bug in "R" formatting on some .NET versions, that means that
// roundtrip actually does not work.
// See section "Note to Callers:" at https://msdn.microsoft.com/en-us/library/kfsatb94(v=vs.110).aspx
// so here we format and then parse back to see if it can actually roundtrip.
// if not, we format with G17.
var d_str = d.ToString("R", CultureInfo.InvariantCulture);
var d_re = double.Parse(d_str, CultureInfo.InvariantCulture);
if (d_re != d)
// round trip not possible with R, use G17 instead.
d_str = d.ToString("G17", CultureInfo.InvariantCulture);
elem.Value = d_str;
return true;
}
case float f:
elem.Value = f.ToString("R", CultureInfo.InvariantCulture);
return true;
case decimal d:
elem.Value = BigFloat.Convert(d).ToString(CultureInfo.InvariantCulture);
return true;
case char c:
// Convert to a string and then handle it later.
obj = Convert.ToString(c, CultureInfo.InvariantCulture);
break;
}
if (obj is string str) // handled separately due to "case char c" from above.
{
// check if the str->xml is reversible otherwise base64 encode it.
if (containsOnlyReversibleTapXmlChars(str) && str.Trim() == str)
{
elem.Value = str;
}
else
{
var subelem = new XElement("Base64", Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str)));
elem.Add(subelem);
}
return true;
}
if (expectedType is TypeData type)
{
if(type.Type.IsEnum || type.Type.IsPrimitive || type.Type.IsValueType)
{
if (type.Type == typeof(bool))
{
elem.Value =
(bool)obj ? "true" : "false"; // must be lower case for old XmlSerializer to work
return true;
}
if (enumTable.TryGetValue(obj, out var v))
{
elem.Value = v;
return true;
}
v = Convert.ToString(obj, CultureInfo.InvariantCulture);
enumTable[obj] = v;
elem.Value = v;
return true;
}
}
IMemberData xmlTextProp = null;
var _type = TypeData.GetTypeData(obj);
var properties = _type.GetMembers();
foreach (IMemberData prop in properties)
{
if (prop.HasAttribute()) continue;
if (prop.HasAttribute())
xmlTextProp = prop;
var attr = prop.GetAttribute();
if (attr != null)
{
var val = prop.GetValue(obj);
var defaultAttr = prop.GetAttribute();
var name = string.IsNullOrWhiteSpace(attr.AttributeName) ? prop.Name : attr.AttributeName;
if (defaultAttr != null && object.Equals(defaultAttr.Value, val))
continue;
string valStr = Convert.ToString(val, CultureInfo.InvariantCulture);
if (val is bool b)
valStr = b ? "true" : "false"; // must be lower case for old XmlSerializer to work
elem.SetAttributeValue(Serializer.PropertyXmlName(name), valStr);
}
}
if (xmlTextProp != null)
{ // XmlTextAttribute support
var text = xmlTextProp.GetValue(obj);
if (text != null)
Serializer.Serialize(elem, text, xmlTextProp.TypeDescriptor);
}
else
{
foreach (IMemberData subProp in properties)
{
if (subProp.HasAttribute()) continue;
if (subProp.Readable && subProp.Writable && null == subProp.GetAttribute())
{
var oldProp = CurrentMember;
CurrentMember = subProp;
try
{
object val = subProp.GetValue(obj);
var enu = val as IEnumerable;
if (enu != null && enu.GetEnumerator().MoveNext() == false) // the value is an empty IEnumerable
{
var defaultAttr = subProp.GetAttribute();
if (defaultAttr != null && defaultAttr.Value == null)
continue;
}
var attr = subProp.GetAttribute();
if (subProp.TypeDescriptor is TypeData cst && cst.Type.HasInterface() &&
cst.Type.IsGenericType && attr != null)
{
// Special case to mimic old .NET XmlSerializer behavior
foreach (var item in enu)
{
string name = attr.ElementName ?? subProp.Name;
XElement elem2 = new XElement(Serializer.PropertyXmlName(name));
SetHasDefaultValueAttribute(subProp, item, elem2);
elem.Add(elem2);
Serializer.Serialize(elem2, item, TypeData.FromType(cst.Type.GetGenericArguments().First()));
}
}
else
{
XElement elem2 = new XElement(Serializer.PropertyXmlName(subProp.Name));
{ // MetaDataAttribute -> save the metadata name in the test plan xml.
if (subProp.GetAttribute() is MetaDataAttribute metaDataAttr)
{
string name = metaDataAttr.Name ??
subProp.GetDisplayAttribute()?.Name ?? subProp.Name;
elem2.SetAttributeValue("Metadata", name);
}
}
// if the setting has the default value, then don't notify that the type has been used,
// because it will probably not have been used in the end.
bool hasDefaultValue = SetHasDefaultValueAttribute(subProp, val, elem2);
elem.Add(elem2);
Serializer.Serialize(elem2, val, subProp.TypeDescriptor, notifyTypeUsed: !hasDefaultValue);
}
}
catch (Exception e)
{
Serializer.PushError(null, $"Unable to serialize property '{subProp.Name}'.", e);
}
finally
{
CurrentMember = oldProp;
}
}
}
}
if (elem.IsEmpty && obj != null)
elem.Value = "";
return true;
}
finally
{
Object = prevObj;
if (!cycleDetetionSet.Remove(obj2))
throw new InvalidOperationException("obj was modified.");
}
}
bool SetHasDefaultValueAttribute(IMemberData subProp, object val, XElement elem2)
{
var attr = subProp.GetAttribute();
if (attr != null && !(subProp is IParameterMemberData))
{
Serializer.GetSerializer().RegisterDefaultValue(elem2, attr.Value);
return val == attr.Value;
}
return false;
}
}
}