438 lines
16 KiB
C#
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; }
|
|
}
|
|
}
|