Files
Umbraco-CMS/src/Umbraco.Infrastructure/PropertyEditors/ConfigurationEditorOfTConfiguration.cs
2020-04-03 11:47:38 +11:00

198 lines
8.0 KiB
C#

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
{
/// <summary>
/// Represents a data type configuration editor with a typed configuration.
/// </summary>
public abstract class ConfigurationEditor<TConfiguration> : ConfigurationEditor
where TConfiguration : new()
{
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationEditor{TConfiguration}"/> class.
/// </summary>
protected ConfigurationEditor(IIOHelper ioHelper)
: base(DiscoverFields(ioHelper))
{ }
/// <summary>
/// Discovers fields from configuration properties marked with the field attribute.
/// </summary>
private static List<ConfigurationField> DiscoverFields(IIOHelper ioHelper)
{
var fields = new List<ConfigurationField>();
var properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration));
foreach (var property in properties)
{
var attribute = property.GetCustomAttribute<ConfigurationFieldAttribute>(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;
}
/// <inheritdoc />
public override IDictionary<string, object> DefaultConfiguration => ToConfigurationEditor(DefaultConfigurationObject);
/// <inheritdoc />
public override object DefaultConfigurationObject => new TConfiguration();
/// <inheritdoc />
public override bool IsConfiguration(object obj)
=> obj is TConfiguration;
/// <inheritdoc />
public override object FromDatabase(string configuration)
{
try
{
if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration();
return JsonConvert.DeserializeObject<TConfiguration>(configuration, ConfigurationJsonSettings);
}
catch (Exception e)
{
throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e);
}
}
/// <inheritdoc />
public sealed override object FromConfigurationEditor(IDictionary<string, object> editorValues, object configuration)
{
return FromConfigurationEditor(editorValues, (TConfiguration) configuration);
}
/// <summary>
/// Converts the configuration posted by the editor.
/// </summary>
/// <param name="editorValues">The configuration object posted by the editor.</param>
/// <param name="configuration">The current configuration object.</param>
public virtual TConfiguration FromConfigurationEditor(IDictionary<string, object> 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<bool>();
if (converted)
o[field.PropertyName] = converted.Result;
}
else
{
//default behavior
o[field.PropertyName] = JToken.FromObject(value);
}
}
}
return o.ToObject<TConfiguration>();
}
/// <inheritdoc />
public sealed override IDictionary<string, object> ToConfigurationEditor(object configuration)
{
return ToConfigurationEditor((TConfiguration) configuration);
}
/// <summary>
/// Converts configuration values to values for the editor.
/// </summary>
/// <param name="configuration">The configuration.</param>
public virtual Dictionary<string, object> ToConfigurationEditor(TConfiguration configuration)
{
string FieldNamer(PropertyInfo property)
{
// try the field
var field = property.GetCustomAttribute<ConfigurationFieldAttribute>();
if (field != null) return field.Key;
// but the property may not be a field just an extra thing
var json = property.GetCustomAttribute<JsonPropertyAttribute>();
return json?.PropertyName ?? property.Name;
}
return ObjectJsonExtensions.ToObjectDictionary(configuration, FieldNamer);
}
}
}