using System; using System.Collections.Generic; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; using Umbraco.Core.IO; namespace Umbraco.Core.PropertyEditors { /// /// Represents a data type configuration editor with a typed configuration. /// public abstract class ConfigurationEditor : ConfigurationEditor where TConfiguration : new() { /// /// Initializes a new instance of the class. /// protected ConfigurationEditor(IIOHelper ioHelper) : base(DiscoverFields(ioHelper)) { } /// /// Discovers fields from configuration properties marked with the field attribute. /// private static List DiscoverFields(IIOHelper ioHelper) { var fields = new List(); var properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration)); foreach (var property in properties) { var attribute = property.GetCustomAttribute(false); if (attribute == null) continue; ConfigurationField field; var attributeView = ioHelper.ResolveRelativeOrVirtualUrl(attribute.View); // if the field does not have its own type, use the base type if (attribute.Type == null) { field = new ConfigurationField { // if the key is empty then use the property name Key = string.IsNullOrWhiteSpace(attribute.Key) ? property.Name : attribute.Key, Name = attribute.Name, PropertyName = property.Name, PropertyType = property.PropertyType, Description = attribute.Description, HideLabel = attribute.HideLabel, View = attributeView }; fields.Add(field); continue; } // if the field has its own type, instantiate it try { field = (ConfigurationField) Activator.CreateInstance(attribute.Type); } catch (Exception ex) { throw new Exception($"Failed to create an instance of type \"{attribute.Type}\" for property \"{property.Name}\" of configuration \"{typeof(TConfiguration).Name}\" (see inner exception).", ex); } // then add it, and overwrite values if they are assigned in the attribute fields.Add(field); field.PropertyName = property.Name; field.PropertyType = property.PropertyType; if (!string.IsNullOrWhiteSpace(attribute.Key)) field.Key = attribute.Key; // if the key is still empty then use the property name if (string.IsNullOrWhiteSpace(field.Key)) field.Key = property.Name; if (!string.IsNullOrWhiteSpace(attribute.Name)) field.Name = attribute.Name; if (!string.IsNullOrWhiteSpace(attribute.View)) field.View = attributeView; if (!string.IsNullOrWhiteSpace(attribute.Description)) field.Description = attribute.Description; if (attribute.HideLabelSettable.HasValue) field.HideLabel = attribute.HideLabel; } return fields; } /// public override IDictionary DefaultConfiguration => ToConfigurationEditor(DefaultConfigurationObject); /// public override object DefaultConfigurationObject => new TConfiguration(); /// public override bool IsConfiguration(object obj) => obj is TConfiguration; /// public override object FromDatabase(string configuration) { try { if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration(); return JsonConvert.DeserializeObject(configuration, ConfigurationJsonSettings); } catch (Exception e) { throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e); } } /// public sealed override object FromConfigurationEditor(IDictionary editorValues, object configuration) { return FromConfigurationEditor(editorValues, (TConfiguration) configuration); } /// /// Converts the configuration posted by the editor. /// /// The configuration object posted by the editor. /// The current configuration object. public virtual TConfiguration FromConfigurationEditor(IDictionary editorValues, TConfiguration configuration) { // note - editorValue contains a mix of CLR types (string, int...) and JToken // turning everything back into a JToken... might not be fastest but is simplest // for now var o = new JObject(); foreach (var field in Fields) { // field only, JsonPropertyAttribute is ignored here // only keep fields that have a non-null/empty value // rest will fall back to default during ToObject() if (editorValues.TryGetValue(field.Key, out var value) && value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue))) { if (value is JToken jtoken) { //if it's a jtoken then set it o[field.PropertyName] = jtoken; } else if (field.PropertyType == typeof(bool) && value is string sBool) { //if it's a boolean property type but a string is found, try to do a conversion var converted = sBool.TryConvertTo(); if (converted) o[field.PropertyName] = converted.Result; } else { //default behavior o[field.PropertyName] = JToken.FromObject(value); } } } return o.ToObject(); } /// public sealed override IDictionary ToConfigurationEditor(object configuration) { return ToConfigurationEditor((TConfiguration) configuration); } /// /// Converts configuration values to values for the editor. /// /// The configuration. public virtual Dictionary ToConfigurationEditor(TConfiguration configuration) { string FieldNamer(PropertyInfo property) { // try the field var field = property.GetCustomAttribute(); if (field != null) return field.Key; // but the property may not be a field just an extra thing var json = property.GetCustomAttribute(); return json?.PropertyName ?? property.Name; } return ObjectJsonExtensions.ToObjectDictionary(configuration, FieldNamer); } } }