// 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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security;
using System.Text;
namespace OpenTap
{
///
/// Plugin interface for string convert providers.
///
///
/// FromString(GetString(value), value.GetType()) shall always return a non-null value, if GetString(value) returns a non-null value.
///
[Display("String Converter")]
public interface IStringConvertProvider : ITapPlugin
{
///
/// Returns a string when the implementation supports converting the value. Otherwise, returns null.
///
/// Cannot be null.
/// The culture used for the conversion.
///
string GetString(object value, CultureInfo culture);
///
/// Creates an object from stringdata. The returned object should be of type 'type'. Returns null if it cannot convert stringdata to type.
///
///
///
/// The object on which the value is set.
/// The culture used for the conversion. This value can will default to InvariantCulture if nothing else is selected.
///
object FromString(string stringData, ITypeData type, object contextObject, CultureInfo culture);
}
///
/// Helper methods for converting to/from strings.
///
public static class StringConvertProvider
{
static class PluginTypeFetcher
{
static readonly Dictionary saveTypes = new Dictionary();
static T[] things = Array.Empty();
public static T[] GetPlugins()
{
var derivedTypes = TypeData.GetDerivedTypes();
int count = derivedTypes.Count(x => x.CanCreateInstance);
if (count != saveTypes.Count)
{
lock (saveTypes)
{
if (count != saveTypes.Count)
{
var builder = things.ToList();
Dictionary orderings = new Dictionary();
foreach (var type in derivedTypes)
{
if (type.CanCreateInstance == false) continue;
if (saveTypes.ContainsKey(type) == false)
{
try
{
var newthing = (T)type.CreateInstance();
saveTypes.Add(type, newthing);
// if BeforeAttribute is used, insert somewhere before the marked type.
// otherwise insert at the end of the list.
int addIndex = builder.Count;
var ordering = type.GetAttributes().ToArray();
orderings[newthing] = ordering;
foreach (var before in ordering)
{
int addIndex2 = builder.IndexWhen(item => before.Before(TypeData.GetTypeData(item)));
if (addIndex2 != -1 && addIndex2 < addIndex)
addIndex = addIndex2;
}
builder.Insert(addIndex, newthing);
}
catch
{
saveTypes[type] = default(T);
} // Ignore errors here.
}
}
// iterate to find the correct ordering.
// this is a bit akin to bubble sorting, but the chance that these dependenceny chains gets very long is limited.
{
HashSet<(T, T)> swapPairs = new HashSet<(T, T)>();
for (int i = 0; i < builder.Count; i++)
{
int addIndex = i;
var type = builder[i];
foreach (var before in orderings[type])
{
int addIndex2 = builder.IndexWhen(item => before.Before(TypeData.GetTypeData(item)));
if (addIndex2 != -1 && addIndex2 < addIndex)
addIndex = addIndex2;
}
if (addIndex < i)
{
if (swapPairs.Add((builder[addIndex], builder[i])) == false)
throw new Exception("Circular dependency in BeforeAttribute"); // these has been swapped before - that means there is a circular dependency.
(builder[i], builder[addIndex]) = (builder[addIndex], builder[i]);
i = addIndex;
}
}
}
things = builder.ToArray();
}
}
}
return things;
}
}
class CachePopularity: IEnumerable
{
class Wrap : IStringConvertProvider
{
IStringConvertProvider inner;
public long Popularity;
public Wrap(IStringConvertProvider inner)
{
this.inner = inner;
}
public string GetString(object value, CultureInfo culture)
{
var str = inner.GetString(value, culture);
if (str != null)
Popularity += 1;
return str;
}
public object FromString(string stringData, ITypeData type, object contextObject, CultureInfo culture)
{
object obj = inner.FromString(stringData, type, contextObject, culture);
if (obj != null)
Popularity += 1;
return obj;
}
}
readonly Wrap[] wrappers;
int[] popularityIndex;
public CachePopularity(IStringConvertProvider[] elements)
{
wrappers = elements.Select(x => new Wrap(x)).ToArray();
popularityIndex = new int[wrappers.Length];
for (int i = 0; i < wrappers.Length; i++)
popularityIndex[i] = i;
}
public IEnumerator GetEnumerator()
{
var popularity = popularityIndex;
long prevPopularity = long.MaxValue;
for(int i = 0; i < popularity.Length; i++)
{
var item = popularity[i];
var wrap = wrappers[item];
if (i > 0 && wrap.Popularity / 4 > prevPopularity && popularityIndex == popularity)
{
var newIndex = popularity.ToArray();
Utils.Swap(ref newIndex[i], ref newIndex[i - 1]);
popularityIndex = newIndex;
}
prevPopularity = wrap.Popularity;
yield return wrap;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
class StringConvertCacheOptimizer : ICacheOptimizer
{
static readonly ThreadField cacheField = new ThreadField();
public void LoadCache() => cacheField.Value = new CachePopularity(PluginTypeFetcher.GetPlugins());
public void UnloadCache() => cacheField.Value = null;
public static IEnumerable GetValue() => (IEnumerable)cacheField.Value ?? PluginTypeFetcher.GetPlugins();
}
static IEnumerable Providers => StringConvertCacheOptimizer.GetValue();
///
/// Turn value to a string if an IStringConvertProvider plugin supports the value. Returns null if the input value is null.
///
/// The value to be converted to a string.
/// If null, invariant culture is used.
///
public static string GetString(object value, CultureInfo culture = null)
{
if (value == null) return null;
if (value is string p) return p;
culture = culture ?? CultureInfo.InvariantCulture;
foreach(var provider in Providers)
{
try {
var str = provider.GetString(value, culture);
if (str != null)
{
return str;
}
}
catch { } // ignore errors. Assume unsupported value.
}
return value.ToString();
}
///
/// Try get a string from an object value.
///
///
///
///
///
public static bool TryGetString(object value, out string str, CultureInfo culture = null)
{
str = null;
if (value == null) return false;
if (value is string p)
{
str = p;
return true;
}
culture = culture ?? CultureInfo.InvariantCulture;
foreach (var provider in Providers)
{
try
{
str = provider.GetString(value, culture);
if (str != null) return true;
}
catch { } // ignore errors. Assume unsupported value.
}
return false;
}
///
/// Turn stringdata back to an object of type 'type', if an IStringConvertProvider plugin supports the string/type.
///
///
///
///
/// If null, invariant culture is used.
///
public static object FromString(string stringdata, ITypeData type, object contextObject, CultureInfo culture = null)
{
if (stringdata == null)
return null;
if (type == null)
throw new ArgumentNullException("type");
culture = culture ?? CultureInfo.InvariantCulture;
foreach (var provider in Providers)
{
try
{
var value = provider.FromString(stringdata, type, contextObject, culture);
if (value != null) return value;
}
catch
{
} // ignore errors. Assume unsupported value.
}
throw new FormatException(string.Format("Unable to parse '{0}' as a {1}.", stringdata, type));
}
///
/// Turn stringdata back to an object of type 'type', if an IStringConvertProvider plugin supports the string/type. returns true if the parsing was successful.
///
///
///
///
/// The result of the operation.
///
///
public static bool TryFromString(string stringdata, ITypeData type, object contextObject, out object result, CultureInfo culture = null)
{
if (type == null)
throw new ArgumentNullException("type");
result = null;
if (stringdata == null)
return false;
culture = culture ?? CultureInfo.InvariantCulture;
foreach (var provider in Providers)
{
try
{
var value = provider.FromString(stringdata, type, contextObject, culture);
if (value != null)
{
result = value;
return true;
}
}
catch
{
} // ignore errors. Assume unsupported value.
}
return false;
}
}
// All the IStringConvertProvider default implementations.
namespace Plugins
{
/// String Convert probider for IConvertible.
internal class ConvertibleStringConvertProvider : IStringConvertProvider
{
/// Returns an IConvertible if applicable.
public object FromString(string stringData, ITypeData _type, object contextObject, CultureInfo culture)
{
var type = (_type as TypeData)?.Type;
if (type == null) return null;
if (type == typeof(DateTime))
{
return DateTime.ParseExact(stringData, "yyyy-MM-dd HH-mm-ss", culture);
}
if (type.IsNumeric())
{
if(new NumberFormatter(culture).TryParseNumber(stringData, type, out object val))
{
return val;
}
return null;
}
else if (type == typeof(string))
return stringData;
else if(type == typeof(bool))
{
if (bool.TryParse(stringData, out bool value))
return value;
return null;
}
if (type.DescendsTo(typeof(IConvertible)) && type.IsEnum == false)
{
try
{
return Convert.ChangeType(stringData, type, culture);
}
catch
{
return null;
}
}
return null;
}
/// Turns an IConvertible into a string.
public string GetString(object value, CultureInfo culture)
{
if (value is Enum)
return null;
if (value is DateTime)
return ((DateTime)value).ToString("yyyy-MM-dd HH-mm-ss");
if (value is IConvertible)
return (string)Convert.ChangeType(value, typeof(string), culture);
return null;
}
}
/// String Convert for Enabled of T.
internal class EnabledStringConvertProvider : IStringConvertProvider
{
static Type GetEnabledElementType(Type enumType)
{
if (enumType.IsArray)
return enumType.GetElementType();
var ienumInterface = enumType.GetInterface("Enabled`1") ?? enumType;
if (ienumInterface != null)
return ienumInterface.GetGenericArguments().FirstOrDefault();
return typeof(object);
}
/// Creates an Enabled from a string.
public object FromString(string stringData, ITypeData _type, object contextObject, CultureInfo culture)
{
Type type = (_type as TypeData)?.Type;
if (type == null) return null;
if (type.DescendsTo(typeof(IEnabledValue)) == false)
return null;
stringData = stringData.Trim();
var disabled = "(disabled)";
IEnabledValue enabled = (IEnabledValue)Activator.CreateInstance(type);
var elemType = GetEnabledElementType(type);
if (stringData.StartsWith(disabled))
{
if (StringConvertProvider.TryFromString(stringData.Substring(disabled.Length).Trim(), TypeData.FromType(elemType), null, out object obj, culture))
enabled.Value = obj;
else return null;
enabled.IsEnabled = false;
}
else
{
if (StringConvertProvider.TryFromString(stringData, TypeData.FromType(elemType), null, out object obj, culture))
enabled.Value = obj;
else return null;
enabled.IsEnabled = true;
}
return enabled;
}
/// Turns Enabled into a string.
public string GetString(object value, CultureInfo culture)
{
if (value is IEnabledValue ev)
{
var inner = StringConvertProvider.GetString(ev.Value, culture);
if (inner == null) return null;
if (ev.IsEnabled)
return inner;
return "(disabled)" + inner;
}
return null;
}
}
/// String Convert for enums.
internal class EnumStringConvertProvider : IStringConvertProvider
{
static bool tryParseEnumString(string str, Type type, out Enum result)
{
result = null;
if (str == "")
{
result = (Enum)Enum.ToObject(type, 0);
return true;
}
if (str.Contains('|'))
{
var flagStrings = str.Split('|');
long flags = 0;
foreach (var flagString in flagStrings)
{
if (tryParseEnumString(flagString, type, out var result2))
flags |= (long)Convert.ChangeType(result2, TypeCode.Int64);
}
result = (Enum)Enum.ToObject(type, flags);
return true;
}
var names = Enum.GetNames(type);
//try
{ // Look for an exact match.
for(int i = 0; i < names.Length; i++)
{
if(names[i] == str)
{
result = (Enum)Enum.GetValues(type).GetValue(i);
return true;
}
}
// try to match display name.
for (int i = 0; i < names.Length; i++)
{
var display = type.GetMember(names[i]).Select(x => x.GetDisplayAttribute()).FirstOrDefault();
if (StringComparer.OrdinalIgnoreCase.Equals(display.Name, str))
{
result = (Enum)Enum.GetValues(type).GetValue(i);
return true;
}
}
}
{// try a more robust parse method. (tolower, trim, '_'=' ')
str = str.Trim().ToLower();
var fixedNames = names.Select(name => name.Trim().ToLower()).ToArray();
for (int i = 0; i < fixedNames.Length; i++)
{
if (fixedNames[i] == str || fixedNames[i].Replace('_', ' ') == str)
{
result = (Enum)Enum.GetValues(type).GetValue(i);
return true;
}
}
// Repeat the robust parse for display names
for (int i = 0; i < names.Length; i++)
{
var display = type.GetMember(names[i]).Select(x => x.GetDisplayAttribute()).FirstOrDefault();
var name = display.Name.ToLower().Trim();
if (StringComparer.OrdinalIgnoreCase.Equals(name, str) ||
StringComparer.OrdinalIgnoreCase.Equals(name.Replace('_', ' '), str))
{
result = (Enum) Enum.GetValues(type).GetValue(i);
return true;
}
}
}
result = null;
return false;
}
/// Creates an enum from a string.
public object FromString(string stringData, ITypeData type, object contextObject, CultureInfo culture)
{
if (type is TypeData cst)
{
if (false == cst.Type.IsEnum) return null;
Enum result;
if (tryParseEnumString(stringData, cst.Type, out result) == false)
return null;
return result;
}
return null;
}
static Dictionary isFlagsAttribute = new Dictionary();
static Array getFlagsValues(Type t)
{
lock (isFlagsAttribute)
{
if (isFlagsAttribute.TryGetValue(t, out Array isFlagsValue))
return isFlagsValue;
Array values = t.HasAttribute() ? Enum.GetValues(t) : null;
isFlagsAttribute[t] = values;
return values;
}
}
static ConcurrentDictionary enumToStringCache = new ConcurrentDictionary();
public string GetString(object value, CultureInfo culture)
{
if (value is Enum e)
{
if (enumToStringCache.TryGetValue(e, out var conv))
return conv;
conv = getString(e);
enumToStringCache[e] = conv;
return conv;
}
return null;
}
/// Turns an enum into a string. Usually just ToString unless flags.
static string getString(Enum val)
{
if (getFlagsValues(val.GetType()) is Array values)
{
StringBuilder sb = new StringBuilder();
bool first = true;
foreach (Enum flag in values)
{
if (val.HasFlag(flag))
{
if (first)
first = false;
else
sb.Append('|');
sb.Append(flag);
}
}
return sb.ToString();
}
return val.ToString();
}
}
/// String convert for list/IEnumerable types.
internal class ListStringConvertProvider : IStringConvertProvider
{
/// Creates a sequence from string.
public object FromString(string stringData, ITypeData _type, object contextObject, CultureInfo culture)
{
var type = (_type as TypeData)?.Type;
if (type.DescendsTo(typeof(IEnumerable)) == false) return null;
if (type.DescendsTo(typeof(string))) return null;
var elemType = type.GetEnumerableElementType();
if (elemType == null) return null;
Array seq = null;
if (elemType.IsNumeric())
{
var fmt = new NumberFormatter(culture);
var data = fmt.Parse(stringData);
if (data.GetType().DescendsTo(type))
return data;
seq = data.CastTo(elemType).Cast