using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace InABox.Core { public interface IBaseColumn { /// /// Unique name of this column. /// /// /// Every column needs a name to distinguish it from other columns, for example in query result tables, or in SQL queries. /// string Name { get; } } public interface IBaseColumns { IEnumerable Columns(); } public interface IComplexColumn : IBaseColumn { IComplexFormulaNode Formula { get; } } public class ComplexColumn : IComplexColumn { public string Name { get; } public IComplexFormulaNode Formula { get; } public ComplexColumn(string name, IComplexFormulaNode formula) { Name = name; Formula = formula; } #region IComplexColumn IComplexFormulaNode IComplexColumn.Formula => Formula; #endregion } public interface IColumn : IBaseColumn { string Property { get; } Type Type { get; } string IBaseColumn.Name => Property; } public static class Column { public static IColumn Create(Type concrete, string property) { var type = typeof(Column<>).MakeGenericType(concrete); var result = Activator.CreateInstance(type, property) as IColumn; return result!; } } public class Column : IColumn { private IProperty _property; public Type Type { get { if (Expression is null) throw new Exception($"Expression [{Property}] may not be null"); if (Expression is IndexExpression) return DatabaseSchema.Property(typeof(T), Property).PropertyType; return Expression.Type; } } public string Property { get; private set; } public Expression Expression { get; private set; } public bool IsEqualTo(string name) => !name.IsNullOrWhiteSpace() && Property.Equals(name); public bool IsEqualTo(Column column) => string.Equals(Property, column.Property); public bool IsParentOf(string name) => !name.IsNullOrWhiteSpace() && name.StartsWith(Property + "."); public Column(IProperty property) { Property = property.Name; Expression = property.Expression(); } public Column(Expression> expression) { Property = CoreUtils.GetFullPropertyName(expression, "."); Expression = CoreUtils.ExtractMemberExpression(expression); } public Column(string property) { Property = property; var iprop = DatabaseSchema.Property(typeof(T), property); if (iprop != null) Expression = iprop.Expression(); else Expression = CoreUtils.CreateMemberExpression(typeof(T), property); } public Column Cast() where TNew: T { return new Column(Property); } public bool TryCast([NotNullWhen(true)] out Column? newColumn) { if(DatabaseSchema.Property(typeof(TNew), Property) is IProperty property) { newColumn = new Column(property); return true; } else { newColumn = null; return false; } } public override string ToString() => Property; } public interface IColumns : ISerializeBinary, IBaseColumns { int Count { get; } IEnumerable ColumnNames(); bool Contains(string column); IColumns Add(string column); IColumns Add(IColumn column); IColumns Add(Expression> column); IColumns Add(IProperty property); IColumns Add(IEnumerable columns); IColumns Add(params string[] columns); IEnumerator GetEnumerator(); } [Flags] public enum ColumnTypeFlags { /// /// No columns at all. /// None = 0, /// /// All columns which are marked as . /// Required = 1, /// /// All columns. /// All = 2, /// /// Only columns on the entity, and not on entity links or calculated fields. /// /// /// This option does include foreign keys. /// Local = 4, IncludeID = 8, IncludeForeignKeys = 16, /// /// Include all columns that are accessible through entity links present in the root class. /// IncludeLinked = 32, /// /// Include all columns that are accessible through entity links, even nested ones. /// IncludeNestedLinks = 64, IncludeAggregates = 128, IncludeFormulae = 256, /// /// Include any columns that are a . /// IncludeUserProperties = 512, IncludeEditable = 1024, /// /// Include all columns marked as , (or that don't have a visibility). /// IncludeOptional = 2048, /// /// Include all columns marked as (or that don't have a visibility). /// IncludeVisible = 4096, DefaultVisible = IncludeVisible | IncludeID, EditorColumns = IncludeID | Required | IncludeOptional | IncludeForeignKeys | IncludeUserProperties | IncludeEditable, } public static class Columns { public static IColumns Create(Type concrete, ColumnTypeFlags flags) { if (!typeof(T).IsAssignableFrom(concrete)) throw new Exception($"Columns: {concrete.EntityName()} does not implement {typeof(T).EntityName()}"); var type = typeof(Columns<>).MakeGenericType(concrete); var result = Activator.CreateInstance(type, flags); return (result as IColumns)!; } public static IColumns Create(Type concrete, ColumnTypeFlags flags) { var type = typeof(Columns<>).MakeGenericType(concrete); var result = Activator.CreateInstance(type, flags) as IColumns; return result!; } /// /// Create a new that is completely empty. /// public static IColumns None(Type T) => Create(T, ColumnTypeFlags.None); /// /// Create a new with all columns of that are marked as . /// public static IColumns Required(Type T) => Create(T, ColumnTypeFlags.Required); /// /// Create a new with all columns local to the entity, and not on entity links or calculated fields. /// public static IColumns Local(Type T) => Create(T, ColumnTypeFlags.Local); /// /// Create a new with all columns that the entity has. /// public static IColumns All(Type T) => Create(T, ColumnTypeFlags.All); /// /// Create a new that is completely empty. /// public static Columns None() { return new Columns(ColumnTypeFlags.None); } /// /// Create a new with all columns of that are marked as . /// public static Columns Required() { return new Columns(ColumnTypeFlags.Required); } /// /// Create a new with all columns local to the entity, and not on entity links or calculated fields. /// public static Columns Local() { return new Columns(ColumnTypeFlags.Local); } /// /// Create a new with all columns that the entity has. /// public static Columns All() { return new Columns(ColumnTypeFlags.All); } } public class Columns : IColumns, ICollection> { #region Private Fields private readonly List> columns; #endregion #region Public Accessors public Column this[int index] => columns[index]; public int Count => columns.Count; bool ICollection>.IsReadOnly => false; #endregion private Columns() { columns = new List>(); } public Columns(ColumnTypeFlags flags) { columns = new List>(); AddColumns(flags); } public Columns AddColumns(ColumnTypeFlags flags) { if (flags == ColumnTypeFlags.None) return this; var props = DatabaseSchema.Properties(typeof(T)) .Where(x => x.Setter() != null) .OrderBy(x => x.PropertySequence()).ToList(); if (flags.HasFlag(ColumnTypeFlags.All)) { foreach (var prop in props) columns.Add(new Column(prop.Name)); return this; } if (typeof(T).IsSubclassOf(typeof(Entity)) && flags.HasFlag(ColumnTypeFlags.IncludeID)) columns.Add(new Column(nameof(Entity.ID))); foreach(var prop in props) { if (flags.HasFlag(ColumnTypeFlags.Required) && prop.Required) { columns.Add(new Column(prop)); } else { var isLocal = !prop.HasParentEntityLink() || (prop.Parent?.HasParentEntityLink() != true && prop.Name.EndsWith(".ID")); if (flags.HasFlag(ColumnTypeFlags.Local) && isLocal && !prop.IsCalculated) { columns.Add(new Column(prop)); } else if(prop is CustomProperty) { if (flags.HasFlag(ColumnTypeFlags.IncludeUserProperties)) { columns.Add(new Column(prop)); } else { // Don't add } } else { var parentLink = prop.HasParentEntityLink(); var failed = false; if(prop.HasParentEntityLink()) { if(prop.Parent?.HasParentEntityLink() == true && !flags.HasFlag(ColumnTypeFlags.IncludeNestedLinks)) { failed = true; } else if(!prop.Name.EndsWith(".ID") && !flags.HasFlag(ColumnTypeFlags.IncludeLinked)) { failed = true; } else if(prop.Name.EndsWith(".ID") && !flags.HasFlag(ColumnTypeFlags.IncludeForeignKeys)) { failed = true; } } if (!failed) { var hasNullEditor = prop.GetParent(x => x.HasEditor && x.Editor is NullEditor) != null; var visible = hasNullEditor ? Visible.Hidden : (prop.Editor?.Visible ?? Visible.Optional); var editable = hasNullEditor ? Editable.Hidden : (prop.Editor?.Editable ?? Editable.Enabled); failed = (!flags.HasFlag(ColumnTypeFlags.IncludeVisible) || visible != Visible.Default) && (!flags.HasFlag(ColumnTypeFlags.IncludeOptional) || (visible != Visible.Optional && visible != Visible.Default)) && (!flags.HasFlag(ColumnTypeFlags.IncludeEditable) || !editable.ColumnVisible()); } if (!failed) { failed = (!flags.HasFlag(ColumnTypeFlags.IncludeAggregates) && prop.HasAttribute()) || (!flags.HasFlag(ColumnTypeFlags.IncludeFormulae) && (prop.HasAttribute() || prop.HasAttribute())); } if (!failed) { columns.Add(new Column(prop)); } } } } return this; } #region IColumns IColumns IColumns.Add(string column) => Add(column); IColumns IColumns.Add(Expression> expression) { return Add(CoreUtils.GetFullPropertyName(expression, ".")); } public IColumns Add(IColumn column) { if (column is Column col) return Add(col); return this; } IColumns IColumns.Add(IProperty property) => Add(property); IColumns IColumns.Add(params string[] columnnames) => Add(columnnames); IColumns IColumns.Add(IEnumerable columnnames) => Add(columnnames); #endregion #region IBaseColumns IEnumerable IBaseColumns.Columns() => columns; #endregion #region Add private Columns Add(string column) { if (!Contains(column)) { if(CoreUtils.TryGetProperty(typeof(T), column, out var propertyInfo)) { columns.Add(new Column(column)); } else { var property = DatabaseSchema.Property(typeof(T), column); if(property is null) { Logger.Send(LogType.Error, "", $"Property {column} does not exist on {typeof(T).Name}"); } else { columns.Add(new Column(property)); } } } return this; } public Columns Add(Column column) { if(!Contains(column.Property)) { columns.Add(column); } return this; } public Columns Add(IProperty property) { if (!Contains(property.Name)) { columns.Add(new Column(property)); } return this; } public Columns Add(Expression> expression) => Add(CoreUtils.GetFullPropertyName(expression, ".")); public Columns Add(Expression> expression) => Add(CoreUtils.GetFullPropertyName(expression, ".")); #region Range Adds public Columns AddSubColumns(Expression> super, Columns? sub) { sub ??= CoreUtils.GetColumns(sub); var prefix = CoreUtils.GetFullPropertyName(super, ".") + "."; foreach(var column in sub.ColumnNames()) { columns.Add(new Column(prefix + column)); } return this; } public Columns Add(IEnumerable> columns) { foreach(var col in columns) { Add(col); } return this; } public Columns Add(params string[] columnnames) { foreach (var name in columnnames) Add(name); return this; } public Columns Add(IEnumerable columnnames) { foreach (var name in columnnames) Add(name); return this; } public Columns Add(params Expression>[] expressions) { foreach (var expression in expressions) columns.Add(new Column(expression)); return this; } /// /// Add a range of columns, without checking for duplicates. /// /// /// public Columns AddRange(IEnumerable> columns) { this.columns.AddRange(columns); return this; } #endregion #endregion #region Remove public Columns Remove(string column) { columns.RemoveAll(x => x.Property == column); return this; } #endregion #region Casting Columns Type public Columns Cast() where TNew : T { var cols = Columns.None(); foreach(var column in columns) { cols.Add(column.Cast()); } return cols; } /// /// Cast the columns to , keeping the columns that are found in both and . /// /// /// public Columns CastIntersection() { var cols = Columns.None(); foreach(var column in columns) { if (column.TryCast(out var newColumn)) { cols.Add(newColumn); } } return cols; } #endregion public override string ToString() { return string.Join("; ", columns.Select(x => x.Property)); } public int IndexOf(string column) { for(int i = 0; i < columns.Count; ++i) { if (columns[i].Property == column) { return i; } } return -1; } public int IndexOf(Expression> expression) { var columnName = CoreUtils.GetFullPropertyName(expression, "."); for(int i = 0; i < columns.Count; ++i) { if (columns[i].Property == columnName) { return i; } } return -1; } public bool Contains(string column) => columns.Any(x => x.IsEqualTo(column)); public IEnumerable ColumnNames() { return columns.Select(c => c.Property); } #region Binary Serialization public void SerializeBinary(CoreBinaryWriter writer) { writer.Write(columns.Count); foreach(var column in columns) { writer.Write(column.Property); } } public void DeserializeBinary(CoreBinaryReader reader) { columns.Clear(); var nColumns = reader.ReadInt32(); for(int i = 0; i < nColumns; ++i) { var property = reader.ReadString(); columns.Add(new Column(property)); } } #endregion #region ICollection public IEnumerator> GetEnumerator() { return columns.GetEnumerator(); } IEnumerator IColumns.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() { return columns.GetEnumerator(); } void ICollection>.Add(Column item) { Add(item); } void ICollection>.Clear() { columns.Clear(); } bool ICollection>.Contains(Column item) { return Contains(item.Property); } void ICollection>.CopyTo(Column[] array, int arrayIndex) { columns.CopyTo(array, arrayIndex); } bool ICollection>.Remove(Column item) { return columns.RemoveAll(x => x.Property == item.Property) > 0; } #endregion } public static class ColumnsExtensions { public static Columns ToColumns(this IEnumerable> columns, ColumnTypeFlags flags) { return new Columns(flags).AddRange(columns); } } public static class ColumnSerialization { /// /// Inverse of . /// /// /// public static Columns? ReadColumns(this CoreBinaryReader reader) { if (reader.ReadBoolean()) { var columns = Columns.None(); columns.DeserializeBinary(reader); return columns; } return null; } /// /// Inverse of . /// /// /// public static void Write(this CoreBinaryWriter writer, Columns? columns) { if (columns is null) { writer.Write(false); } else { writer.Write(true); columns.SerializeBinary(writer); } } } public class ColumnJsonConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { if(value is null) { writer.WriteNull(); return; } var property = (CoreUtils.GetPropertyValue(value, "Expression") as Expression) ?? throw new Exception("'Column.Expression' may not be null"); var prop = CoreUtils.ExpressionToString(value.GetType().GenericTypeArguments[0], property, true); var name = CoreUtils.GetPropertyValue(value, "Property") as string; writer.WriteStartObject(); writer.WritePropertyName("$type"); writer.WriteValue(value.GetType().FullName); writer.WritePropertyName("Expression"); writer.WriteValue(prop); writer.WritePropertyName("Property"); writer.WriteValue(name); writer.WriteEndObject(); } public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var data = new Dictionary(); while (reader.TokenType != JsonToken.EndObject && reader.Read()) if (reader.Value != null) { var key = reader.Value.ToString(); reader.Read(); if (String.Equals(key, "$type")) objectType = Type.GetType(reader.Value.ToString()) ?? objectType; else data[key] = reader.Value; } var prop = data["Property"].ToString(); var result = Activator.CreateInstance(objectType, prop); return result; } public override bool CanConvert(Type objectType) { if (objectType.IsConstructedGenericType) { var ot = objectType.GetGenericTypeDefinition(); var tt = typeof(Column<>); if (ot == tt) return true; } return false; } } }