using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace InABox.Core { public static class DatabaseSchema { // {className: {propertyName: property}} private static Dictionary> _properties = new Dictionary>(); private struct SubObject { public Type PropertyType { get; set; } public string Name { get; set; } public Action Setter { get; set; } public SubObject(Type objectType, Type propertyType, string name) { PropertyType = propertyType; Name = name; Setter = Expressions.Setter(objectType, name); } } private static Dictionary> _subObjects { get; } = new Dictionary>(); private static List? GetSubObjects(Type t) { CheckProperties(t); return _subObjects.GetValueOrDefault(t); } public static void InitializeSubObjects(BaseObject obj) { var objs = GetSubObjects(obj.GetType()); if(objs is null) { return; } foreach (var subObjectDef in objs) { var subObj = (Activator.CreateInstance(subObjectDef.PropertyType) as ISubObject)!; subObjectDef.Setter(obj, subObj); subObj.SetLinkedParent(obj); subObj.SetLinkedPath(subObjectDef.Name); } } private static void RegisterSubObject(Type objectType, Type propertyType, string name) { lock (_updatelock) { if (!_subObjects.TryGetValue(objectType, out var properties)) { properties = new List(); _subObjects[objectType] = properties; } if (!properties.Any(x => x.Name == name && x.PropertyType == propertyType)) { properties.Add(new SubObject(objectType, propertyType, name)); } } } private static void RegisterProperties(Type master, Type type, string prefix, StandardProperty? parent) { try { var classname = master.EntityName(); var properties = CoreUtils.PropertyList( type, x => !x.PropertyType.IsInterface && x.GetGetMethod()?.IsPublic == true && (x.DeclaringType.IsSubclassOf(typeof(BaseObject)) || x.DeclaringType.IsSubclassOf(typeof(BaseEditor))) ); var classProps = _properties.GetValueOrDefault(classname); foreach (var prop in properties) { var name = string.IsNullOrWhiteSpace(prefix) ? prop.Name : string.Format("{0}.{1}", prefix, prop.Name); var p = classProps?.GetValueOrDefault(name); if (p == null) { var isstatic = prop.GetAccessors(true)[0].IsStatic; if (!isstatic) { BaseEditor? editor; if (parent != null && parent.HasEditor && parent.Editor is NullEditor) { editor = parent.Editor; } else { editor = prop.GetEditor(); } var captionAttr = prop.GetCustomAttribute(); var subCaption = captionAttr != null ? captionAttr.Text : prop.Name; var path = captionAttr == null || captionAttr.IncludePath; // If no caption attribute, we should always include the path var caption = parent?.Caption ?? ""; // We default to the parent caption if subCaption doesn't exist if (!string.IsNullOrWhiteSpace(subCaption)) { if (!string.IsNullOrWhiteSpace(caption) && path) { caption = $"{caption} {subCaption}"; } else { caption = subCaption; } } // Once the parent page has been found, this property is cemented to that page - it cannot change page to its parent // The same goes for sequence var page = parent?.Page; var sequence = parent?.Sequence; if (string.IsNullOrWhiteSpace(page)) { var sequenceAttribute = prop.GetCustomAttribute(); if (sequenceAttribute != null) { page = sequenceAttribute.Page; sequence = sequenceAttribute.Sequence; } } editor = editor?.Clone() as BaseEditor; if (editor != null) { editor.Page = page; editor.Caption = caption; editor.EditorSequence = (int)(sequence ?? 999); editor.Security = prop.GetCustomAttributes().ToArray(); } bool required = false; if (parent == null || parent.Required) { required = prop.GetCustomAttribute() != null; } LoggablePropertyAttribute? loggable = null; if (parent == null || parent.Loggable != null) { loggable = prop.GetCustomAttribute(); } var newProperty = new StandardProperty { _class = master, //Class = classname, Name = name, PropertyType = prop.PropertyType, Editor = editor ?? new NullEditor(), HasEditor = editor != null, Caption = caption, Sequence = sequence ?? 999, Page = page ?? "", Required = required, Loggable = loggable, Parent = parent, Property = prop }; var parentWithEditable = newProperty.GetOuterParent(x => x is StandardProperty st && st.Property.GetCustomAttribute() != null); if(parentWithEditable != null) { var attr = (parentWithEditable as StandardProperty)!.Property.GetCustomAttribute()!; newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable); } else if(prop.GetCustomAttribute() is EditableAttribute attr) { newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable); } var isLink = prop.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)); var isEnclosedEntity = prop.PropertyType.GetInterfaces().Contains(typeof(IEnclosedEntity)); var isBaseEditor = prop.PropertyType.Equals(typeof(BaseEditor)) || prop.PropertyType.IsSubclassOf(typeof(BaseEditor)); if ((isLink || isEnclosedEntity) && !isBaseEditor) { RegisterSubObject(type, prop.PropertyType, prop.Name); } if (isLink || isEnclosedEntity || isBaseEditor) { RegisterProperties(master, prop.PropertyType, name, newProperty); } else { RegisterProperty(newProperty); } } } } if (type.IsSubclassOf(typeof(BaseObject))) RegisterProperties(master, type.BaseType, prefix, parent); } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } } private static void RegisterProperties(Type type) { RegisterProperties(type, type, "", null); } public static object? DefaultValue(Type type) { if (type.IsValueType) return Activator.CreateInstance(type); if (type.Equals(typeof(string))) return ""; return null; } private static readonly object _updatelock = new object(); public static void RegisterProperty(IProperty entry) { lock (_updatelock) { if (!_properties.ContainsKey(entry.Class)) _properties[entry.Class] = new Dictionary(); _properties[entry.Class][entry.Name] = entry; } } public static void Load(CustomProperty[] customproperties) { foreach (var prop in customproperties) RegisterProperty(prop); } private static void CheckProperties(Type type) { var entityName = type.EntityName(); try { var props = _properties.GetValueOrDefault(entityName); var hasprops = props?.Any(x => x.Value is StandardProperty) == true; if (type.IsSubclassOf(typeof(BaseObject)) && !hasprops) RegisterProperties(type); } catch (Exception e) { // This seems to be an intermittent error "Collection has been modified" when checking if the Dictionary has been populated already // I've added a .ToArray() to concretise the list, but who knows? Logger.Send(LogType.Error,"",$"Error Checking Properties for Type: {entityName}\n{e.Message}\n{e.StackTrace}"); } } public static IEnumerable Properties(Type type) { CheckProperties(type); var entityName = type.EntityName(); return _properties.GetValueOrDefault(entityName)?.Select(x => x.Value) ?? Array.Empty(); } public static IProperty? Property(Type type, string name) { CheckProperties(type); var entityName = type.EntityName(); var prop = _properties.GetValueOrDefault(entityName)?.GetValueOrDefault(name); // Walk up the inheritance tree, see if an ancestor has this property if (prop == null && type.BaseType != null) prop = Property(type.BaseType, name); return prop; } public static IProperty? Property(Expression> expression) => Property(typeof(T), CoreUtils.GetFullPropertyName(expression, ".")); public static void InitializeObject(TObject entity) where TObject : BaseObject { entity.UserProperties.Clear(); var props = Properties(entity.GetType()).Where(x => x is CustomProperty).ToArray(); foreach (var field in props) entity.UserProperties[field.Name] = DefaultValue(field.PropertyType); } } }