// 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().ToArray(); } else { // unescape nested strings and put them into 'parsedStrings'. // after this they will be handled individually. List parsedStrings = new List(); if (stringData.Length > 0) { StringBuilder buffer = new StringBuilder(); for (int i = 0; i < stringData.Length; i++) { var chr = stringData[i]; if (chr == ',') { parsedStrings.Add(buffer.ToString()); buffer.Clear(); continue; } if (chr == '"') { for (i += 1; i < stringData.Length; i++) { chr = stringData[i]; if (chr == '"') { if (stringData.Length > i + 1 && stringData[i + 1] == '"') i += 1; // skip escaped quotes and remove extra \". else break; } buffer.Append(chr); } } else { buffer.Append(chr); } } parsedStrings.Add(buffer.ToString()); } var values = new List(); foreach (var str in parsedStrings) { if (StringConvertProvider.TryFromString(str.Trim(), TypeData.FromType(elemType), null, out object e)) { values.Add(e); } else { // cannot handle element -> give up. return null; } } seq = values.ToArray(); } if (seq == null) return null; if (type.IsArray) { var array = Array.CreateInstance(elemType, seq.Length); int idx = 0; foreach (var item in seq) array.SetValue(item, idx++); return array; } else if (type.GetConstructor([]) != null) { object lst = Activator.CreateInstance(type); var add = type.GetMethodsTap().First(m => m.Name == "Add"); foreach (object item in seq) add.Invoke(lst, new[] { item }); return lst; } else { return null; } } NumberFormatter fmt = new NumberFormatter(CultureInfo.InvariantCulture) { UseRanges = false}; /// Turns a value into a string, public string GetString(object value, CultureInfo culture) { /* return null if we do not know how to construct this type. Another provider may be able to handle this, so this is fine. */ { var type = value?.GetType(); if (type == null) return null; if (type.DescendsTo(typeof(string))) return null; if (type.IsArray == false && type.GetConstructor([]) == null) return null; } if (value is IEnumerable seq) { bool isNumeric = false; foreach (var elem in seq) { isNumeric = true; if (elem is Enum || Utils.IsNumeric(elem) == false) { isNumeric = false; break; } } if (isNumeric) return fmt.FormatRange(seq); string escapeString(string str) { if (str.Contains(",") || str.Contains("\"")) return $"\"{str.Replace("\"", "\"\"")}\""; return str; } var sb = new StringBuilder(); bool first = true; foreach (var val in seq) { if (StringConvertProvider.TryGetString(val, out string result, culture)) { if (first) first = false; else sb.Append(", "); sb.Append(escapeString(result)); } else { // Signal that the thing could not be converted. return null; } } return sb.ToString(); } return null; } } /// /// String convert for IResource types. /// internal class ResourceStringConvertProvider : IStringConvertProvider { IEnumerable _allResources = null; IEnumerable allResources { get { if(_allResources == null) { var ilist = TypeData.FromType(typeof(IList)); var cmplists = TypeData.FromType(typeof(ComponentSettings)).DerivedTypes.Where(x => x.DescendsTo(ilist)); var reslists = cmplists.Where(x => x.ElementType.DescendsTo(typeof(IResource))).ToArray(); _allResources = reslists.SelectMany(x => ComponentSettings.GetCurrent(x.Type) as IEnumerable); } return _allResources; } } /// Finds a IResource based on strings. Only works on things loaded in Component Settings. public object FromString(string stringData, ITypeData type, object contextObject, CultureInfo culture) { if (type.DescendsTo(typeof(IResource)) == false) return null; if (type is TypeData) return allResources.FirstOrDefault(x => x.Name == stringData && TypeData.GetTypeData(x).DescendsTo(type)); return null; } /// Turns a resource into a string. public string GetString(object value, CultureInfo culture) { if (value is IResource) return ((IResource)value).Name; return null; } } /// String convert provider for MacroString types. internal class MacroStringConvertProvider : IStringConvertProvider { /// Creates a new MacroString, using the contextObject if its a ITestStep. public object FromString(string stringData, ITypeData type, object contextObject, CultureInfo culture) { if (type.DescendsTo(typeof(MacroString)) == false) return null; return new MacroString(contextObject as ITestStepParent) { Text = stringData }; } /// Extracts the text component of a macro string. public string GetString(object value, CultureInfo culture) { if (value is MacroString == false) return null; return ((MacroString)value).Text; } } /// String convert provider for SecureString internal class SecureStringConvertProvider : IStringConvertProvider { /// Creates a new SecureString public object FromString(string stringData, ITypeData targetType, object contextObject, CultureInfo culture) { if (targetType.IsA(typeof(SecureString)) == false) return null; return stringData.ToSecureString(); } /// Extracts the text component of a SecureString. public string GetString(object obj, CultureInfo culture) { if (obj is SecureString == false) return null; return ((SecureString)obj).ConvertToUnsecureString(); } } /// String convert provider for TestStep internal class TestStepConvertProvider : IStringConvertProvider { internal static ITestStep GetStepFromContext(ITestStepParent context, Guid id) { ITestStepParent plan = context; while (plan.Parent != null) plan = plan.Parent; return Utils.FlattenHeirarchy(plan.ChildTestSteps, x => x.ChildTestSteps) .FirstOrDefault(x => x.Id == id); } /// /// Gets a TestStep from a string value. This will be a step from the test plan context object. /// /// /// /// /// /// public object FromString(string stringData, ITypeData type, object contextObject, CultureInfo culture) { if (type.DescendsTo(typeof(ITestStep)) && contextObject is ITestStepParent parent) { if(Guid.TryParse(stringData, out Guid id)) { return GetStepFromContext(parent, id); } } return null; } /// /// Gets the ID of a step. /// /// /// /// public string GetString(object value, CultureInfo culture) { return (value as ITestStep)?.Id.ToString(); } } /// Supports converting Inputs to a string and back. Requires the context to be an ITestStep. internal class InputStringConvertProvider : IStringConvertProvider { /// Creates an Input from a string. contextObject must be an ITestStep. /// /// /// /// /// public object FromString(string stringData, ITypeData type, object contextObject, CultureInfo culture) { if (!type.DescendsTo(typeof(IInput))) return null; var s = stringData.Split(':'); var step = contextObject as ITestStep; if (step == null) return null; if (s.Length != 2) return null; if (!Guid.TryParse(s[0], out var id)) return null; var property = s[1]; var inp = (IInput)type.CreateInstance(Array.Empty()); if (id == Guid.Empty) return inp; var step2 = TestStepConvertProvider.GetStepFromContext(step, id); if (step2 == null) return null; inp.Step = step2; if (!string.IsNullOrWhiteSpace(property)) inp.Property = TypeData.GetTypeData(step2).GetMember(property); return inp; } /// Turns an IInput into a string. /// /// /// public string GetString(object value, CultureInfo culture) { var val = value as IInput; if (val == null) return null; return $"{val.Step?.Id ?? Guid.Empty}:{val.Property?.Name ?? ""}"; } } /// Supports converting Inputs to a string and back. internal class BoolConverter : IStringConvertProvider { /// Creates a bool from a string. public object FromString(string stringData, ITypeData type, object ctx, CultureInfo culture) { if (type.IsA(typeof(bool))) { var sd = stringData.ToLower(); switch (sd) { case "true": case "y": case "yes": return true; case "false": case "n": case "no": return false; } } return null; } /// Gets the string representation of a bool. public string GetString(object value, CultureInfo culture) { if (value is bool) return value.ToString(); else return null; } } } }