Files
certmgr/certmgr/Core/SettingsBuilder.cs
2025-10-18 21:43:09 +02:00

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