using InABox.Clients; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace InABox.Core { public interface ILookupDefinition { IFilter? DefineFilter(); IColumns DefineColumns(); IColumns RequiredColumns(); ISortOrder? DefineSortOrder(); string FormatLookup(Dictionary values, IEnumerable exclude); } public interface ILookupDefinition : ILookupDefinition where TLookup : BaseObject, new() { new Filter? DefineFilter(); new Columns DefineColumns(); new Columns RequiredColumns(); new SortOrder? DefineSortOrder(); IFilter? ILookupDefinition.DefineFilter() => DefineFilter(); IColumns ILookupDefinition.DefineColumns() => DefineColumns(); IColumns ILookupDefinition.RequiredColumns() => RequiredColumns(); ISortOrder? ILookupDefinition.DefineSortOrder() => DefineSortOrder(); } public interface IEntityLookup where TGenerator : ILookupDefinition, new() where TLookup : BaseObject, new() { } public interface IChildEntityLookup where TGenerator : LookupDefinitionGenerator where TParent : class where TChild : class { } /// /// Define a lookup definition for a given property. The generator must derive from . /// public class LookupDefinitionAttribute : Attribute { public Type Generator { get; set; } public LookupDefinitionAttribute(Type generator) { if (!generator.IsSubclassOfRawGeneric(typeof(LookupDefinitionGenerator<,>))) { throw new Exception($"{generator} is not a {typeof(LookupDefinitionGenerator<,>)}"); } Generator = generator; } } public interface ILookupDefinitionGenerator { public IColumns DefineColumns(); public IFilter? DefineFilter(BaseObject[] items); public IColumns DefineFilterColumns(); public ISortOrder? DefineSortOrder(); public void OnCreateItem(BaseObject[] parent, BaseObject lookup); } public abstract class LookupDefinitionGenerator : ILookupDefinitionGenerator where TEntity: class where TLookup: class { public virtual Columns DefineColumns() => Columns.None(); public virtual Filter? DefineFilter(TEntity[] items) => null; /// /// Define the columns required for the items parameter of . /// /// public virtual Columns DefineFilterColumns() => Columns.None(); /// /// Customise an item created by a lookup editor. /// /// /// public virtual void OnCreateItem(TEntity[] parent, TLookup lookup) { } public virtual SortOrder? DefineSortOrder() => null; void ILookupDefinitionGenerator.OnCreateItem(BaseObject[] parents, BaseObject lookup) => OnCreateItem(parents as TEntity[], lookup as TLookup); IColumns ILookupDefinitionGenerator.DefineColumns() => DefineColumns(); IFilter? ILookupDefinitionGenerator.DefineFilter(BaseObject[] items) => DefineFilter(items as TEntity[]); IColumns ILookupDefinitionGenerator.DefineFilterColumns() => DefineFilterColumns(); ISortOrder? ILookupDefinitionGenerator.DefineSortOrder() => DefineSortOrder(); } public static class LookupFactory { private static Dictionary? _lookupDefinitions; private static readonly Dictionary _lookupGenerators = new Dictionary(); private static readonly Dictionary, Type> _childGenerators = new Dictionary, Type>(); public static void RegisterLookupGenerator(Expression> expression) where TGenerator : LookupDefinitionGenerator where TLookupLink : IEntityLink where TLookup: class where TEntity: class { _lookupGenerators.TryAdd(DatabaseSchema.Property(typeof(TEntity), CoreUtils.GetFullPropertyName(expression, ".") + ".ID")!.Parent!, typeof(TGenerator)); } public static void RegisterChildGenerator() where TGenerator : LookupDefinitionGenerator where TChild: class where TParent: class { _childGenerators.TryAdd(new Tuple(typeof(TParent), typeof(TChild)), typeof(TGenerator)); } private static ILookupDefinition? GetDefinition(Type T) { var type = T.GetInterfaceDefinition(typeof(IEntityLookup<,>))?.GenericTypeArguments[1]; if(type is null) { _lookupDefinitions ??= CoreUtils.TypeList(x => !x.IsAbstract && !x.IsGenericType && x.HasInterface(typeof(ILookupDefinition<>))) .Select(x => new Tuple(x, x.GetInterfaceDefinition(typeof(ILookupDefinition<>))!)) .ToDictionary(x => x.Item2.GenericTypeArguments[0], x => x.Item1); type = _lookupDefinitions.GetValueOrDefault(T); } return type != null ? (Activator.CreateInstance(type) as ILookupDefinition) : null; } private static IProperty? GetLinkProperty(Type T, string column) { if (column.EndsWith(".ID")) { var property = DatabaseSchema.Property(T, column); if(property?.Parent?.IsEntityLink == true) { return property.Parent; } } return DatabaseSchema.Property(T, $"{column}.{nameof(IEntityLink.ID)}")?.Parent; } private static ILookupDefinitionGenerator? GetLookupGenerator(IProperty? property) { var generator = property?.GetAttribute()?.Generator ?? (property != null ? _lookupGenerators.GetValueOrDefault(property) : null); return generator != null ? Activator.CreateInstance(generator) as ILookupDefinitionGenerator : null; } private static ILookupDefinitionGenerator? GetLookupGenerator(Type T, string column) { return GetLookupGenerator(GetLinkProperty(T, column)); } private static ILookupDefinitionGenerator? GetChildGenerator(Type TParent, Type TChild) { var generator = TParent.GetInterfaces(typeof(IChildEntityLookup<,,>)) .FirstOrDefault(x => x.GenericTypeArguments[0] == TParent && x.GenericTypeArguments[1] == TChild)?.GenericTypeArguments[2] ?? _childGenerators.GetValueOrDefault(new Tuple(TParent, TChild)); return generator != null ? Activator.CreateInstance(generator) as ILookupDefinitionGenerator : null; } #region Non-generic functions /// /// Define a generic filter for , not in the context of a parent object. For use in multi-select windows and stuff. /// /// /// public static IFilter? DefineFilter(Type TLookup) { return GetDefinition(TLookup)?.DefineFilter(); } /// /// Define a specific filter for in the context of (one or more) , /// specifically for property , which should be an or the ID property of an . /// This is the standard function for lookups. /// /// /// can either be the name of an entity link, or the ID property of that entity link. /// /// /// /// /// public static IFilter? DefineLookupFilter(Type TEntity, Type TLookup, string column, BaseObject[] items) { var generator = GetLookupGenerator(TEntity, column); var filter = generator?.DefineFilter(items); return filter ?? GetDefinition(TLookup)?.DefineFilter(); } /// /// Define a specific filter for in the context of (one or more) , /// specifically for child entities. /// /// /// /// /// public static IFilter? DefineChildFilter(Type TEntity, Type TLookup, BaseObject[] items) { var generator = GetChildGenerator(TEntity, TLookup); var filter = generator?.DefineFilter(items); return filter ?? GetDefinition(TLookup)?.DefineFilter(); } /// /// Define the columns needed for the entities for . /// /// /// /// public static IColumns DefineLookupFilterColumns(Type TEntity, string column) { return GetLookupGenerator(TEntity, column)?.DefineFilterColumns() ?? Columns.None(TEntity); } /// /// Define the columns needed for the entities for . /// /// /// public static IColumns DefineChildFilterColumns(Type TEntity, Type TLookup) { return GetChildGenerator(TEntity, TLookup)?.DefineFilterColumns() ?? Columns.None(TEntity); } /// /// Define a generic set of columns for , not in the context of a parent object. For use in multi-select windows and stuff. /// /// /// public static IColumns DefineColumns(Type TLookup) { return GetDefinition(TLookup)?.DefineColumns() ?? Columns.Create(TLookup, ColumnTypeFlags.IncludeID | ColumnTypeFlags.IncludeVisible); } /// /// Get a list of columns that need to be loaded in a lookup for children of on . /// /// /// /// public static IColumns DefineChildColumns(Type TEntity, Type TLookup) { var columns = Columns.None(TLookup); foreach(var prop in DatabaseSchema.Properties(TLookup).Where(x => x.Required)) { columns.Add(prop.Name); } // Add any columns defined on a LookupDefinitionAttribute var generator = GetChildGenerator(TEntity, TLookup); if(generator != null) { foreach (var prop in generator.DefineColumns()) { columns.Add(prop); } } // Finally, add any columns defined on an ILookupDefinition for this entity. if(GetDefinition(TLookup) is ILookupDefinition definition) { foreach(var prop in definition.DefineColumns()) { columns.Add(prop); } } return columns; } /// /// Get a list of columns that need to be loaded in a lookup for property on . /// /// /// can either be the name of an entity link, or the ID property of that entity link.
/// Gives an aggregate of required columns on the entity link in question, the linked properties of , and any defined on /// a or . ///
/// /// /// /// public static IColumns DefineLookupColumns(Type TEntity, Type TLookup, string column) { var columns = Columns.None(TLookup); var property = GetLinkProperty(TEntity, column); if(property != null) { var prefix = property.Name + "."; // Add all required columns of the entity link. foreach(var prop in DatabaseSchema.Properties(TEntity).Where(x => x.Required && x.Name.StartsWith(prefix))) { columns.Add(prop.Name[prefix.Length..]); } // Add all columns which are associated with linked properties. foreach(var lProp in LinkedProperties.FindAll(TEntity).Where(x => x.Path.StartsWith(prefix))) { var parent = lProp.Path[prefix.Length..]; columns.Add((parent.IsNullOrWhiteSpace() ? "" : $"{parent}.") + lProp.Source); } // Add any columns defined on a LookupDefinitionAttribute var generator = GetLookupGenerator(property); if(generator != null) { foreach (var prop in generator.DefineColumns()) { columns.Add(prop); } } } // Finally, add any columns defined on an ILookupDefinition for this entity. if(GetDefinition(TLookup) is ILookupDefinition definition) { foreach(var prop in definition.DefineColumns()) { columns.Add(prop); } } if (columns.Count == 0) { columns = Columns.Create(TLookup, ColumnTypeFlags.IncludeID | ColumnTypeFlags.IncludeVisible); } return columns; } /// /// Get a list of columns that need to be loaded for an entity of type . /// /// /// Gives an aggregate of required columns, and columns found on and . /// /// /// public static IColumns RequiredColumns(Type T) { var result = Columns.None(T); var props = DatabaseSchema.Properties(T); foreach (var prop in DatabaseSchema.Properties(T)) { if (prop.Required) { // First, add all the required properties, as calculated by DatabaseSchema. result.Add(prop.Name); } else if(prop.Name.EndsWith(".ID")) { // We also want to load columns on lookup definitions, which are attributes on entity link classes. // EntityLinks must have an ID property, so we use that to ensure we only get one property from the entity link, // since we actually care about the parent. var parentLink = prop.GetOuterParent(x => x.IsEntityLink); if(parentLink != null && parentLink == prop.Parent) { // We need here to ensure that the outermost parent link is the parent of this property, otherwise we're multiple nested entitylinks deep, // and we don't want that. if(parentLink.GetAttribute() is LookupDefinitionAttribute attr) { foreach (var column in (Activator.CreateInstance(attr.Generator) as ILookupDefinitionGenerator)!.DefineColumns()) { result.Add(parentLink.Name + "." + column.Property); } } } } } // We also need to load all the columns defined on lookup definitions; if we've defined it on a lookup, // it means it's probably necessary for a calculation of some kind, and therefore we should also have it when we load the object. if(GetDefinition(T) is ILookupDefinition definition) { foreach(var column in definition.RequiredColumns()) { result.Add(column); } } return result; } public static string DefaultFormatLookup(Dictionary values, IEnumerable exclude) { return string.Join(": ", values.Where(x => x.Value != null && x.Value.GetType() != typeof(Guid) && (exclude == null || !exclude.Contains(x.Key))) .Select(p => p.Value)); } public static string FormatLookup(Type T, Dictionary values, IEnumerable exclude) { if(GetDefinition(T) is ILookupDefinition definition) { return definition.FormatLookup(values, exclude); } return DefaultFormatLookup(values, exclude); } public static ISortOrder? DefineSort(Type T) { return GetDefinition(T)?.DefineSortOrder(); } public static void OnCreateItem(Type TEntity, string column, BaseObject[] items, BaseObject item) { var generator = GetLookupGenerator(TEntity, column); generator?.OnCreateItem(items, item); } #endregion #region Generic Wrappers public static Filter DefineFilter() => (DefineFilter(typeof(TLookup)) as Filter)!; public static Filter? DefineChildFilter(TEntity[] items) where TEntity : BaseObject => (DefineChildFilter(typeof(TEntity), typeof(TLookup), items) as Filter)!; public static Filter? DefineLookupFilter(Expression> column, TEntity[] items) where TLookupLink : IEntityLink where TEntity : BaseObject => (DefineLookupFilter(typeof(TEntity), typeof(TLookup), CoreUtils.GetFullPropertyName(column, "."), items) as Filter)!; public static Columns DefineChildFilterColumns() => (DefineChildFilterColumns(typeof(TEntity), typeof(TLookup)) as Columns)!; public static Columns DefineLookupFilterColumns(Expression> column) where TLookupLink : IEntityLink => (DefineLookupFilterColumns(typeof(TEntity), CoreUtils.GetFullPropertyName(column, ".")) as Columns)!; public static Columns DefineColumns() => (DefineColumns(typeof(TLookup)) as Columns)!; public static Columns DefineChildColumns() => (DefineChildColumns(typeof(TEntity), typeof(TLookup)) as Columns)!; public static Columns DefineLookupColumns(Expression> column) where TLookupLink : IEntityLink => (DefineLookupColumns(typeof(TEntity), typeof(TLookup), CoreUtils.GetFullPropertyName(column, ".")) as Columns)!; public static Columns RequiredColumns() => (RequiredColumns(typeof(TLookup)) as Columns)!; public static SortOrder? DefineSort() => DefineSort(typeof(TLookup)) as SortOrder; public static string FormatLookup(Dictionary values, IEnumerable exclude) => FormatLookup(typeof(T), values, exclude); public static void OnCreateItem(Expression> column, TEntity[] items, TLookup item) where TLookupLink : IEntityLink where TEntity : BaseObject where TLookup : BaseObject => OnCreateItem(typeof(TEntity), CoreUtils.GetFullPropertyName(column, "."), items, item); public static void DoLookup(TEntity entity, Expression> column, Guid lookupID) where TEntity : class where TLookup : Entity, IRemotable, IPersistent, new() where TLookupLink : IEntityLink { if(lookupID == Guid.Empty) { return; } var columns = DefineLookupColumns(column); var row = Client.Query(new Filter(x => x.ID).IsEqualTo(lookupID), columns).Rows.FirstOrDefault(); if(row is null) { Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in DoLookup: {typeof(TLookup).Name} with ID {lookupID} not found."); return; } foreach(var subColumn in columns) { CoreUtils.SetPropertyValue(entity, CoreUtils.GetFullPropertyName(column, ".") + "." + subColumn.Property, row[subColumn.Property]); } } public static void DoLookups(IEnumerable> lookups, Expression> column) where TEntity : class where TLookup : Entity, IRemotable, IPersistent, new() where TLookupLink : IEntityLink { lookups = lookups.AsIList(); var lookupIDs = lookups.Select(x => x.Item2).Where(x => x != Guid.Empty).ToArray(); var columns = DefineLookupColumns(column).Add(x => x.ID); var data = Client.Query(new Filter(x => x.ID).InList(lookupIDs), columns).Rows.ToDictionary(x => x.Get(x => x.ID)); foreach(var (entity, lookupID) in lookups) { if(lookupID == Guid.Empty) { continue; } if(!data.TryGetValue(lookupID, out var row)) { Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in DoLookup: {typeof(TLookup).Name} with ID {lookupID} not found."); continue; } foreach(var subColumn in columns) { CoreUtils.SetPropertyValue(entity, CoreUtils.GetFullPropertyName(column, ".") + "." + subColumn.Property, row[subColumn.Property]); } } } #endregion } public abstract class BaseObjectLookup : ILookupDefinition where TLookup : BaseObject, new() { public virtual string FormatLookup(Dictionary values, IEnumerable exclude) { var filtered = new Dictionary(); var cols = DefineColumns(); foreach (var col in cols) if (values.ContainsKey(col.Property) && values[col.Property] != null && values[col.Property]?.GetType() != typeof(Guid)) filtered[col.Property] = values[col.Property]; return LookupFactory.DefaultFormatLookup(filtered, exclude); } public abstract Columns DefineColumns(); public abstract Columns RequiredColumns(); public abstract Filter? DefineFilter(); public abstract SortOrder DefineSortOrder(); } public abstract class EntityLookup : BaseObjectLookup where TLookup : Entity, new() { public override Columns DefineColumns() { return new Columns(ColumnTypeFlags.IncludeID); } public override Columns RequiredColumns() { return Columns.None(); } } }