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);
}
}
}