345 lines
12 KiB
C#
345 lines
12 KiB
C#
using System.Collections;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.Reflection;
|
|
|
|
using CertMgr.Core.Attributes;
|
|
using CertMgr.Core.Cli;
|
|
using CertMgr.Core.Converters;
|
|
using CertMgr.Core.Jobs;
|
|
using CertMgr.Core.Log;
|
|
using CertMgr.Core.Utils;
|
|
using CertMgr.Core.Validation;
|
|
|
|
namespace CertMgr.Core;
|
|
|
|
internal sealed class SettingsBuilder
|
|
{
|
|
private readonly RawArguments _rawArgs;
|
|
private readonly Type _settingsType;
|
|
|
|
internal SettingsBuilder(RawArguments rawArgs, Type settingsType)
|
|
{
|
|
_rawArgs = rawArgs;
|
|
_settingsType = settingsType;
|
|
}
|
|
|
|
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
|
|
{
|
|
JobSettings settings = CreateSettingsInstance();
|
|
|
|
foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute())
|
|
{
|
|
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
|
|
{
|
|
(bool isCollection, Type elementType) = GetValueType(propertyInfo);
|
|
|
|
try
|
|
{
|
|
(bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, propertyInfo.PropertyType, elementType, cancellationToken).ConfigureAwait(false);
|
|
if (converted)
|
|
{
|
|
propertyInfo.SetValue(settings, convertedValue);
|
|
|
|
if (settingAttribute.Validator != null)
|
|
{
|
|
ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
|
|
if (validator != null)
|
|
{
|
|
ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false);
|
|
settings.ValidationResults.Add(valres);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
|
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
|
}
|
|
}
|
|
else if (settingAttribute.IsMandatory)
|
|
{
|
|
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
|
|
settings.ValidationResults.Add(valres);
|
|
CLog.Error("mandatory argument '{0}' is missing", settingAttribute.Name);
|
|
}
|
|
else if (settingAttribute.Default != null)
|
|
{
|
|
(bool isCollection, Type elementType) = GetValueType(propertyInfo);
|
|
if (settingAttribute.Default.GetType() == elementType)
|
|
{
|
|
propertyInfo.SetValue(settings, settingAttribute.Default);
|
|
}
|
|
else
|
|
{
|
|
CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", settingAttribute.Name, settingAttribute.Default?.GetType().ToString(false) ?? "<null>", elementType.ToString(false));
|
|
}
|
|
}
|
|
}
|
|
|
|
await settings.ValidateAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
return settings;
|
|
}
|
|
|
|
private bool TryGetRawArgument(SettingAttribute settingAttribute, [NotNullWhen(true)] out RawArgument? rawArg)
|
|
{
|
|
rawArg = null;
|
|
|
|
if (!_rawArgs.TryGet(settingAttribute.Name, out rawArg))
|
|
{
|
|
if (settingAttribute.AlternateNames != null)
|
|
{
|
|
foreach (string altName in settingAttribute.AlternateNames)
|
|
{
|
|
if (_rawArgs.TryGet(altName, out rawArg))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return rawArg != null;
|
|
}
|
|
|
|
private (bool isCollection, Type elementType) GetValueType(PropertyInfo propertyInfo)
|
|
{
|
|
(bool isCollection, Type? elemType) = UnwrapCollection(propertyInfo.PropertyType);
|
|
Type targetType = isCollection ? elemType! : propertyInfo.PropertyType;
|
|
return (isCollection, targetType);
|
|
}
|
|
|
|
private IEnumerable<(PropertyInfo, SettingAttribute)> GetPropertiesWithSettingAttribute()
|
|
{
|
|
foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
SettingAttribute? settingAttribute = propertyInfo.GetCustomAttribute<SettingAttribute>();
|
|
if (settingAttribute is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return (propertyInfo, settingAttribute);
|
|
}
|
|
}
|
|
|
|
private JobSettings CreateSettingsInstance()
|
|
{
|
|
object? instance;
|
|
try
|
|
{
|
|
instance = Activator.CreateInstance(_settingsType);
|
|
if (instance == null)
|
|
{
|
|
throw new CertMgrException("Failed to create instance for settings of type '{0}'", _settingsType.Name);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new CertMgrException(e, "Failed to create instance for settings of type '{0}'", _settingsType.Name);
|
|
}
|
|
|
|
if (instance is not JobSettings settings)
|
|
{
|
|
throw new CertMgrException("Failed to create instance for settings of type '{0}'. The type is not of type '{1}'", _settingsType.Name, typeof(JobSettings).Name);
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
|
|
{
|
|
bool success = false;
|
|
object? convertedValue = null;
|
|
|
|
if (isCollection)
|
|
{
|
|
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
|
{
|
|
Type listType = typeof(List<>).MakeGenericType(elementType);
|
|
IList values = (IList)Activator.CreateInstance(listType)!;
|
|
|
|
foreach (string rawValue in rawArg.Values)
|
|
{
|
|
convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
|
|
values.Add(convertedValue);
|
|
}
|
|
convertedValue = values;
|
|
success = true;
|
|
}
|
|
else
|
|
{
|
|
Type listType = typeof(List<>).MakeGenericType(elementType);
|
|
IList values = (IList)Activator.CreateInstance(listType)!;
|
|
|
|
foreach (string rawValue in rawArg.Values)
|
|
{
|
|
if (TryConvertValue(rawValue, elementType, out convertedValue))
|
|
{
|
|
values.Add(convertedValue);
|
|
}
|
|
}
|
|
convertedValue = values; // BuildCollectionValue(collectionType, elementType, values);
|
|
success = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
|
{
|
|
convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), elementType, cancellationToken).ConfigureAwait(false);
|
|
success = true;
|
|
}
|
|
else if (TryConvertValue(rawArg.Values.First(), elementType, out convertedValue))
|
|
{
|
|
success = true;
|
|
}
|
|
else
|
|
{
|
|
}
|
|
}
|
|
|
|
return (success, convertedValue);
|
|
}
|
|
|
|
private static object BuildCollectionValue(Type collectionType, Type elementType, IReadOnlyList<object?> items)
|
|
{
|
|
// convert source collection with 'items' of type 'object?' to collection with items of requested type:
|
|
Type listType = typeof(List<>).MakeGenericType(elementType);
|
|
IList typedList = (IList)Activator.CreateInstance(listType)!;
|
|
|
|
foreach (object? item in items)
|
|
{
|
|
typedList.Add(item);
|
|
}
|
|
|
|
if (collectionType.IsArray)
|
|
{
|
|
Array array = Array.CreateInstance(elementType, typedList.Count);
|
|
typedList.CopyTo(array, 0);
|
|
return array;
|
|
}
|
|
|
|
// it is either IEnumerable<T> or ICollection<T> or IList<T> or List<T>
|
|
if (collectionType.IsAssignableFrom(listType))
|
|
{
|
|
return typedList;
|
|
}
|
|
|
|
// we do not know the type, but try to instantiate 'collectionType' and if it implements ICollection<>, then fill it
|
|
object? instance = Activator.CreateInstance(collectionType);
|
|
if (instance != null)
|
|
{
|
|
Type? iCollectionT = collectionType
|
|
.GetInterfaces()
|
|
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>));
|
|
|
|
if (iCollectionT != null)
|
|
{
|
|
MethodInfo? add = iCollectionT.GetMethod("Add");
|
|
if (add != null)
|
|
{
|
|
foreach (object? item in typedList)
|
|
{
|
|
add.Invoke(instance, new object?[] { item });
|
|
}
|
|
return instance;
|
|
}
|
|
}
|
|
}
|
|
|
|
// try it as array, we fail anyway
|
|
Array fallback = Array.CreateInstance(elementType, typedList.Count);
|
|
typedList.CopyTo(fallback, 0);
|
|
return fallback;
|
|
}
|
|
|
|
private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter)
|
|
{
|
|
customConverter = null;
|
|
|
|
Type? valueConverter = settingAttribute.Converter;
|
|
if (valueConverter != null)
|
|
{
|
|
if (typeof(IValueConverter).IsAssignableFrom(valueConverter))
|
|
{
|
|
customConverter = (IValueConverter?)Activator.CreateInstance(valueConverter);
|
|
}
|
|
else
|
|
{
|
|
CLog.Error("Argument '{0}' has converter specified but its type doesn't implement '{1}' and cannot be used", settingAttribute.Name, typeof(IValueConverter));
|
|
}
|
|
}
|
|
|
|
return customConverter != null;
|
|
}
|
|
|
|
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
|
|
{
|
|
convertedValue = null;
|
|
|
|
if (targetType.IsEnum)
|
|
{
|
|
if (Enum.TryParse(targetType, rawValue, true, out object? result))
|
|
{
|
|
convertedValue = result;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TypeConverter converter = TypeDescriptor.GetConverter(targetType);
|
|
if (converter.CanConvertFrom(typeof(string)))
|
|
{
|
|
convertedValue = converter.ConvertFrom(null, CultureInfo.InvariantCulture, rawValue)!;
|
|
}
|
|
else if (targetType == typeof(string))
|
|
{
|
|
convertedValue = rawValue;
|
|
}
|
|
else
|
|
{
|
|
convertedValue = null;
|
|
}
|
|
}
|
|
|
|
return convertedValue != null;
|
|
}
|
|
|
|
private static (bool isCollection, Type? elementType) UnwrapCollection(Type type)
|
|
{
|
|
if (type == typeof(string))
|
|
{
|
|
return (false, null);
|
|
}
|
|
|
|
Type? underlying = Nullable.GetUnderlyingType(type);
|
|
if (underlying != null)
|
|
{
|
|
type = underlying;
|
|
}
|
|
|
|
if (type.IsArray)
|
|
{
|
|
return (true, type.GetElementType()!);
|
|
}
|
|
|
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
|
{
|
|
return (true, type.GetGenericArguments()[0]);
|
|
}
|
|
|
|
foreach (Type i in type.GetInterfaces())
|
|
{
|
|
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
|
{
|
|
return (true, i.GetGenericArguments()[0]);
|
|
}
|
|
}
|
|
|
|
return (false, null);
|
|
}
|
|
}
|