Files
certmgr/certmgr/Core/SettingsBuilder.cs
2025-10-21 10:49:38 +02:00

438 lines
16 KiB
C#

using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
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;
}
private static IReadOnlyList<Type> GetValidatedTypes(Type validatorType)
{
if (validatorType == null)
{
throw new ArgumentNullException(nameof(validatorType));
}
List<Type> hits = new List<Type>();
if (validatorType.IsInterface && validatorType.IsGenericType && validatorType.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
{
Type candidate = validatorType.GetGenericArguments()[0];
hits.Add(candidate);
}
foreach (Type iface in validatorType.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
{
Type candidate = iface.GetGenericArguments()[0];
if (!hits.Contains(candidate))
{
hits.Add(candidate);
}
}
}
return hits;
}
private async Task<AsyncResult<object?>> SetPropertyValueAsync(JobSettings settings, PropertyInfo propertyInfo, TypeInfo propertyType, SettingAttribute settingAttribute, CancellationToken cancellationToken)
{
object? convertedValue = null;
bool valueSet = false;
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
{
try
{
AsyncResult<object?> conversionResult = await ConvertRawValueAsync(settingAttribute, rawArg, propertyType.IsCollection, propertyInfo.PropertyType, propertyType.ElementType, cancellationToken).ConfigureAwait(false);
if (conversionResult.IsSuccess)
{
propertyInfo.SetValue(settings, conversionResult.Value);
convertedValue = conversionResult.Value;
valueSet = true;
}
}
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)
{
TypeInfo typeInfo = UnwrapCollection(propertyInfo.PropertyType);
if (settingAttribute.Default.GetType() == typeInfo.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>", typeInfo.ElementType.ToString(false));
}
}
return new AsyncResult<object?>(valueSet, convertedValue);
}
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
{
JobSettings settings = CreateSettingsInstance();
foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute())
{
TypeInfo propertyType = UnwrapCollection(propertyInfo.PropertyType);
AsyncResult<object?> setPropertyResult = await SetPropertyValueAsync(settings, propertyInfo, propertyType, settingAttribute, cancellationToken).ConfigureAwait(false);
if (setPropertyResult.IsSuccess)
{
try
{
if (settingAttribute.Validator != null)
{
IReadOnlyList<Type> validatedTypes = GetValidatedTypes(settingAttribute.Validator);
ISettingValidator? validatorInst = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
if (propertyType.IsCollection)
{
TypeInfo validatedTypeInfo = UnwrapCollection(validatedTypes[0]);
if (validatedTypeInfo.IsCollection)
{
// validator validates collection => send whole collection as argument to ValidateAsync(..)
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
}
else
{
// validator validates elements => send items one by one to ValidateAsync(..)
if (setPropertyResult.Value is IList list)
{
foreach (object value in list)
{
ValidationResult valres = await validatorInst.ValidateAsync(value, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
}
}
}
}
else
{
// setting is not a collection. lets assume the validator doesn't validate collection as well
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
}
}
}
catch (Exception e)
{
CLog.Error(e, "Failed to validate 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));
}
}
}
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 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<AsyncResult<object?>> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
{
bool success = false;
object? convertedValue = null;
IValueConverter? customConverter = GetCustomConverter(settingAttribute);
if (isCollection)
{
Type listType = typeof(List<>).MakeGenericType(elementType);
IList values = (IList)Activator.CreateInstance(listType)!;
foreach (string rawValue in rawArg.Values)
{
AsyncResult<object?> converted = await ConvertValueAsync(rawValue, customConverter, elementType, cancellationToken).ConfigureAwait(false);
if (converted.IsSuccess)
{
values.Add(converted.Value);
}
}
convertedValue = values;
success = true;
}
else
{
AsyncResult<object?> converted = await ConvertValueAsync(rawArg.Values.First(), customConverter, elementType, cancellationToken).ConfigureAwait(false);
if (converted.IsSuccess)
{
convertedValue = converted.Value;
success = true;
}
else
{
CLog.Error("Cannot convert value '{0}' of argument '{1}' to type '{2}'", rawArg.Values.First(), settingAttribute.Name, elementType.ToString(false));
}
}
return new AsyncResult<object?>(success, convertedValue);
}
private async Task<AsyncResult<object?>> ConvertValueAsync(string rawValue, IValueConverter? customConverter, Type elementType, CancellationToken cancellationToken)
{
bool success;
object? convertedValue;
if (customConverter != null)
{
convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
success = true;
}
else
{
success = TryConvertValue(rawValue, elementType, out convertedValue);
}
return new AsyncResult<object?>(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 IValueConverter? GetCustomConverter(SettingAttribute settingAttribute)
{
IValueConverter? 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;
}
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
{
convertedValue = null;
Type? underlying = Nullable.GetUnderlyingType(targetType);
if (underlying != null)
{
targetType = underlying;
}
if (targetType.IsEnum)
{
if (Enum.TryParse(targetType, rawValue, true, out object? result) && Enum.IsDefined(targetType, 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 TypeInfo UnwrapCollection(Type type)
{
if (type == typeof(string))
{
return new TypeInfo(false, type);
}
Type testedType = type;
Type? underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
testedType = underlying;
}
if (testedType.IsArray)
{
return new TypeInfo(true, testedType.GetElementType()!);
}
if (testedType.IsGenericType && testedType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return new TypeInfo(true, testedType.GetGenericArguments()[0]);
}
foreach (Type i in testedType.GetInterfaces())
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return new TypeInfo(true, i.GetGenericArguments()[0]);
}
}
return new TypeInfo(false, type);
}
private sealed class TypeInfo
{
public TypeInfo(bool isCollection, Type elementType)
{
IsCollection = isCollection;
ElementType = elementType;
}
public bool IsCollection { [DebuggerStepThrough] get; }
public Type ElementType { [DebuggerStepThrough] get; }
}
}