using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text; using JD.Collections; using JD.Collections.Extensions; using JD.Kernel.Classes; using JD.Kernel.Interfaces; using JD.Kernel.StaticHelpers; using JD.Reflection; using JD.Serializers.Xml; namespace JD.PropertyService { /// /// A collection of properties and sub-properties. /// [Serializable] public class Properties : IProperties, ICloneable { #region Constants private const string PropertiesElement = "Properties"; private const string PropertyName = "Property"; private const string NameAttribute = "name"; private const string ValueAttribute = "value"; private const string TypeAttribute = "type"; #endregion #region Variables private readonly PropertyCollection m_properties = new PropertyCollection(); private readonly IProperties m_parent; private int m_isUpdatingCount; private readonly TypeConverterCache _typeConverter = new TypeConverterCache(); #endregion #region Constructor /// /// Default creation constructor. /// public Properties() : this(null, null) { } private Properties(Properties other, IProperties parent) { m_parent = parent; if (other != null) { other.CopyTo(this); } } #endregion #region Properties public bool IsUpdating { get { return m_isUpdatingCount > 0; } } /// /// Whether these properties are read-only. /// public virtual bool IsReadOnly { get { return false; } } /// /// Number of values. /// public int Count { get { return m_properties.Count; } } public IEnumerable Keys { get { return m_properties.Select(v => v.Key); } } public IEnumerable Values { get { return m_properties.Select(v => v.Value); } } /// /// Values. /// public IEnumerable ValueKeys { get { return m_properties.Select(v => v.Key); } } /// /// Parent of these properties, if any. /// IReadOnlyProperties IReadOnlyProperties.Parent { get { return Parent; } } /// /// Root of these properties. /// IReadOnlyProperties IReadOnlyProperties.Root { get { return Root; } } /// /// Gets the value of the property with the given . /// If no property exists with that key, is returned. /// /// Key of the property to get/set. /// The value of the property. /// Thrown if is null. public string this[string key] { get { return Get(key, string.Empty); } set { Set(key, value); } } object IReadOnlyDictionary.this[string key] { get { return this[key]; } } /// /// Parent of these properties, if any. /// public IProperties Parent { get { return m_parent; } } /// /// Root of these properties, if any. /// public IProperties Root { get { return m_parent == null ? this : m_parent.Root; } } public IEnumerable SubKeys { get { return m_properties.Where(pair => pair.Value is Properties).Select(pair => pair.Key); } } #endregion #region Methods #region Get / Set /// /// Gets the value of the property with the given . /// If no property exists with that key, is added and returned. /// /// Key of the property to get. /// Default value to use is the property is not found. /// Options to use to retrieve the property. /// The value of the property. /// Thrown if is null. public T Get(string key, T defaultValue, PropertyOptions options) { Throw.IfNull(key, "key"); if (!m_properties.ContainsKey(key)) { if (typeof(T) == typeof(string) && !ReferenceEquals(defaultValue, null) && options != PropertyOptions.DontExpandValue) { return (T)(object)StringParserService.StringParserService.Parse(defaultValue.ToString()); } return defaultValue; } return GetImpl(key, defaultValue, options, true); } /// /// Gets the value of the property with the given . /// If no property exists with that key, is added and returned. /// /// Key of the property to get. /// Default value to use is the property is not found. /// The value of the property. /// Thrown if is null. public T Get(string key, T defaultValue) { return Get(key, defaultValue, PropertyOptions.None); } /// /// Gets the value of the property with the given . /// If no property exists with that key, an exception is thrown. /// /// Key of the property to get. /// The value of the property. /// Thrown if is null. /// Thrown if the doesn't exist in these properties. public T Get(string key) { Throw.IfNull(key, "key"); if (!m_properties.ContainsKey(key)) { throw new KeyNotFoundException(); } return GetImpl(key, default(T), PropertyOptions.None, false); } /// /// Gets sub-properties with the given . /// /// Key to get the properties for. /// The sub-properties for the given or null if not found. /// Thrown if is empty. IReadOnlyProperties IReadOnlyProperties.GetSubKey(string key) { return GetSubKey(key); } /// /// Creates sub-properties with the given or opens existing one. /// /// Key to get the properties for. /// /// The sub-properties for the given or null if could not create a key (in a read-only instance for example). /// Thrown if is empty. IReadOnlyProperties IReadOnlyProperties.CreateSubKey(string key, AccessContext mode) { return CreateSubKey(key, mode); } /// /// Try gets the value of the property with the given . /// /// /// /// /// public bool TryGet(string key, out T value) { Throw.IfNull(key, "key"); ValueHolder result; if (m_properties.TryGetValue(key, out result)) { value = (T)result.Value; return true; } value = default(T); return false; } private T GetImpl(string key, T defaultValue, PropertyOptions options, bool safeCastInstance) { ValueHolder valueHolder = m_properties[key]; Debug.Assert(valueHolder != null); object o = valueHolder.Value; if (o is string && typeof (T) != typeof (object)) { if (typeof (T) != typeof (string)) { TypeConverter c = _typeConverter.GetConverter(typeof (T)); try { o = c.ConvertFromInvariantString(o.ToString()); } catch (Exception) { if (safeCastInstance) { o = defaultValue; } else { throw new InvalidCastException(); } } Remove(key, false); Set(key, o, false); } else if (options != PropertyOptions.DontExpandValue) { o = StringParserService.StringParserService.Parse(o.ToString()); } } if (typeof(T) != typeof(object) && !typeof(T).IsAssignableFrom(o.GetType())) { return defaultValue; } return (T)o; } /// /// Sets the value of the property with the given . /// /// Key of the property to set. /// Value of the property. /// Thrown if or is null. /// Thrown when is true. public void Set(string key, T value) { Set(key, value, true); } private void Set(string key, T value, bool triggerEvent) { Throw.IfNull(key, "key"); Throw.IfNull(value, "value"); T oldValue = default(T); if (m_properties.ContainsKey(key)) { oldValue = GetImpl(key, oldValue, PropertyOptions.None, true); if (Equals(oldValue, value)) { return; } } if (triggerEvent && !OnPropertyChanging()) { return; } m_properties.Replace(new ValueHolder(key, value)); if (triggerEvent) { OnPropertyChanged(new PropertyChangedEventArgs(this, key, oldValue, value)); } } #endregion #region Hierarchy /// /// Creates sub-properties with the given or opens existing one. /// /// Key to get the properties for. /// /// The sub-properties for the given or null if not found. /// Thrown if is empty. public IProperties CreateSubKey(string key, AccessContext mode) { return GetSubKey(key, mode, true); } /// /// Gets sub-properties with the given . /// /// Key to get the properties for. /// The sub-properties for the given or null if not found. /// Thrown if is empty. /// Thrown if contains spaces. public IProperties GetSubKey(string key) { return GetSubKey(key, AccessContext.Read, false); } private IProperties GetSubKey(string key, AccessContext mode, bool createIfNotFound) { Throw.IfEmpty(key, "key"); string[] tokens = key.Split(new [] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); if (tokens.Length == 0) { throw new ArgumentException("Invalid key."); } Properties propertiesToReturn = null; Properties currentProperties = this; foreach (string subKey in tokens) { currentProperties = propertiesToReturn = currentProperties.GetSubKeyImpl(subKey, mode, createIfNotFound); if (propertiesToReturn == null) { break; } } return propertiesToReturn; } private Properties GetSubKeyImpl(string subKey, AccessContext mode, bool createIfNotFound) { Properties propertiesToReturn; if (!TryGet(subKey, out propertiesToReturn) && createIfNotFound) { propertiesToReturn = new Properties(null, this); Set(subKey, propertiesToReturn, mode == AccessContext.Write); } return propertiesToReturn; } /// /// Attempts to remove the sub-key with the given . /// /// Name of the sub-key to remove. /// PropertyChanged event is called when removed if the mode is in write. /// True if the sub-key existed and was removed. False if there is no sub-key with the name. public bool RemoveSubKey(string key, AccessContext mode) { if (key == null) { return false; } return Remove(key, mode == AccessContext.Write); } #endregion #region Other /// /// Returns a read-only version of these properties. /// /// public IReadOnlyProperties AsReadOnly() { return new ReadOnlyProperties(this); } /// /// Whether the properties contain the specified value. /// /// Name of the property to check for. /// True if the value exists. False otherwise. public bool Contains(string key) { return m_properties.ContainsKey(key); } /// /// Attempts to remove the property with the given . /// /// Name of the property to remove. /// True if the property existed and was removed. False if there is no property with the name. public bool Remove(string key) { return Remove(key, true); } private bool Remove(string key, bool triggerChange) { if (key == null || !m_properties.ContainsKey(key)) { return false; } if (triggerChange && !OnPropertyChanging()) { return false; } object oldValue = m_properties[key]; m_properties.Remove(key); if (triggerChange) { OnPropertyChanged(new PropertyChangedEventArgs(this, key, oldValue, null)); } return true; } /// /// Clears all the properties and sub-keys. (Use with caution). /// public void Clear() { if (!OnPropertyChanging()) { return; } int count = m_properties.Count; m_properties.Clear(); if (count != 0) { OnPropertyChanged(new PropertyChangedEventArgs(this, null, null, null)); } } /// /// Overriden. /// /// public override string ToString() { StringBuilder builder = new StringBuilder("[Properties "); if (IsReadOnly) { builder.Append("(ReadOnly)"); } builder.Append(": "); foreach (string value in ValueKeys) { builder.Append(value); builder.Append('='); builder.Append(Get(value).ToString()); builder.Append(", "); } builder.Remove(builder.Length - 2, 2); builder.Append("]"); return builder.ToString(); } public IEnumerator> GetEnumerator() { return m_properties.Select(v => new KeyValuePair(v.Key, v.Value)).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// True if the content equals the other (does not implement equals to be sure it is still performant in a hash). /// /// /// public bool IsEqualTo(IReadOnlyProperties other) { Properties properties = other as Properties; if (properties == null) { return false; } return m_properties.IsEquivalentTo(properties.m_properties, new ValueComparer()); } /// /// Adds the values to the target. /// public void CopyTo(IProperties other) { Throw.IfNull(other, "other"); foreach (var value in m_properties.ThreadSafeEnumerate) { other.Set(value.Key, CloneValue(value.Value, other)); } } /// /// Try to save the file. /// /// Returns true if executed. public virtual bool Save() { return false; } #endregion #region Xml Serialization public void WriteToXml(XmlSerializerWriter serializer) { WriteToXml(serializer, true); } /// /// Writes to xml /// /// /// Whether to encapsulate writing into an element. public virtual void WriteToXml(XmlSerializerWriter serializer, bool encapsulate) { using (encapsulate ? serializer.StartElement(PropertiesElement) : null) { foreach (var pair in m_properties) { using (serializer.StartElement(PropertyName)) { WritePropertyToXml(serializer, pair.Key, pair.Value); } } } } private static void WritePropertyToXml(XmlSerializerWriter serializer, string propertyName, object value) { serializer.Serialize(NameAttribute, propertyName); if (value is Properties) { ((Properties)value).WriteToXml(serializer, false); } else { serializer.Serialize(TypeAttribute, TypeToString(value.GetType())); serializer.Serialize(ValueAttribute, value); } } public virtual void ReadFromXml(XmlSerializerReader serializer) { using (OpenEnclosingIfAny(serializer)) { foreach (string elementName in serializer) { if (elementName == PropertyName && serializer.AttributeExists(NameAttribute)) { ReadPropertyFromXml(serializer); } } } } private static IDisposable OpenEnclosingIfAny(XmlSerializerReader serializer) { if (serializer.NextElementIs(PropertiesElement)) { return serializer.StartElement(); } return null; } private void ReadPropertyFromXml(XmlSerializerReader serializer) { string propertyName = serializer.Serialize(NameAttribute); object value; if (serializer.AttributeExists(ValueAttribute)) { string type = serializer.Serialize(TypeAttribute); value = serializer.Serialize(ValueAttribute, ConvertType(type)); } else { Properties properties; if (!serializer.AttributeExists(TypeAttribute)) { properties = new Properties(null, this); } else { Guid guid = new Guid(serializer.Serialize(TypeAttribute)); throw new InvalidProgramException("A type must be registered on guid " + guid); } properties.ReadFromXml(serializer); value = properties; } Set(propertyName, value); } private static string TypeToString(Type type) { if (type == typeof(bool)) { return "Bool"; } if (type == typeof(float)) { return "Float"; } if (type == typeof(int)) { return "Int"; } if (type == typeof(string)) { return "String"; } return type.FullName; } private static Type ConvertType(string typeName) { // cute name support. switch (typeName) { case "Bool": return typeof(bool); case "Float": return typeof(float); case "Int": return typeof(int); case "String": case "complex": case "": return typeof(string); } Type type = Type.GetType(typeName); if (type == null) { return typeof(string); } return type; } #endregion #region Clone /// /// Clones the current properties. /// /// /// This cloning is shallow. /// /// public virtual Properties Clone() { return new Properties(this, null); } /// /// Clone /// public Properties Clone(IProperties container) { return new Properties(this, container); } object ICloneable.Clone() { return Clone(); } private static object CloneValue(object value, IProperties container) { Properties propertiesToClone = value as Properties; if (propertiesToClone != null) { return propertiesToClone.Clone(container); } return value; } #endregion #region Bulk Update /// /// Starts an update that does not trigger the changed event /// /// public IDisposable BeginScopedUpdate() { m_isUpdatingCount++; return new DisposableHelper(EndUpdate); } /// /// Ends a scoped update /// public void EndUpdate() { Throw.InvalidOperationIf(!IsUpdating, "No update active!"); m_isUpdatingCount--; if (!IsUpdating) { OnPropertyChanged(new PropertyChangedEventArgs(this, null, null, null)); } } #endregion #region Events /// /// Raised before a property changes. /// /// False if cancelled (if the property cannot be changed) protected virtual bool OnPropertyChanging() { if (IsUpdating) // Don't raise event PropertyChanged when updating. { return true; } CancelEventArgs e = new CancelEventArgs(); PropertyChanging?.Invoke(this, e); return !e.Cancel; } /// /// Raised before a property changes. /// [field: NonSerialized] public event EventHandler PropertyChanging; /// /// Triggered when the value of a property changes. /// [field: NonSerialized] public event EventHandler PropertyChanged; /// /// Overridden. Don't raise event PropertyChanged when updating. /// protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { Properties parent = m_parent as Properties; if (parent != null) { parent.OnPropertyChanged(e); } if (!IsUpdating) { PropertyChanged?.Invoke(this, e); } } #endregion #endregion #region Nested private class ValueComparer : IEqualityComparer { bool IEqualityComparer.Equals(ValueHolder x, ValueHolder y) { if (!string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase)) { return false; } if (x.Value is Properties) { return (y.Value is Properties) && (((Properties)x.Value).IsEqualTo((Properties)y.Value)); } return Equals(x.Value, y.Value); } public int GetHashCode(ValueHolder obj) { if (obj.Value is Properties) { return ((Properties)obj.Value).Count; } return obj.Value.GetHashCode(); } } bool IReadOnlyDictionary.TryGetValue(string key, out object value) { return TryGet(key, out value); } public bool ContainsKey(string key) { return m_properties.ContainsKey(key); } private class ValueHolder { public readonly string Key; public readonly object Value; public ValueHolder(string key, object value) { Key = key; Value = value; } } [Serializable] private class PropertyCollection : KeyedCollection { public PropertyCollection() : base(StringComparer.OrdinalIgnoreCase) {} protected override string GetKeyForItem(ValueHolder value) { return value.Key; } } #endregion } }