JD2022-TU1/main/tools/framework/JD.PropertyService/Properties.cs

936 lines
28 KiB
C#

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
{
/// <summary>
/// A collection of properties and sub-properties.
/// </summary>
[Serializable]
public class Properties : IProperties, ICloneable<Properties>
{
#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
/// <summary>
/// Default creation constructor.
/// </summary>
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; }
}
/// <summary>
/// Whether these properties are read-only.
/// </summary>
public virtual bool IsReadOnly
{
get { return false; }
}
/// <summary>
/// Number of values.
/// </summary>
public int Count
{
get
{
return m_properties.Count;
}
}
public IEnumerable<string> Keys
{
get { return m_properties.Select(v => v.Key); }
}
public IEnumerable<object> Values
{
get { return m_properties.Select(v => v.Value); }
}
/// <summary>
/// Values.
/// </summary>
public IEnumerable<string> ValueKeys
{
get { return m_properties.Select(v => v.Key); }
}
/// <summary>
/// Parent of these properties, if any.
/// </summary>
IReadOnlyProperties IReadOnlyProperties.Parent
{
get { return Parent; }
}
/// <summary>
/// Root of these properties.
/// </summary>
IReadOnlyProperties IReadOnlyProperties.Root
{
get { return Root; }
}
/// <summary>
/// Gets the value of the property with the given <paramref name="key"/>.
/// If no property exists with that key, <see cref="string.Empty"/> is returned.
/// </summary>
/// <param name="key">Key of the property to get/set.</param>
/// <returns>The value of the property.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
public string this[string key]
{
get
{
return Get(key, string.Empty);
}
set
{
Set(key, value);
}
}
object IReadOnlyDictionary<string, object>.this[string key]
{
get { return this[key]; }
}
/// <summary>
/// Parent of these properties, if any.
/// </summary>
public IProperties Parent
{
get
{
return m_parent;
}
}
/// <summary>
/// Root of these properties, if any.
/// </summary>
public IProperties Root
{
get
{
return m_parent == null ? this : m_parent.Root;
}
}
public IEnumerable<string> SubKeys
{
get
{
return m_properties.Where(pair => pair.Value is Properties).Select(pair => pair.Key);
}
}
#endregion
#region Methods
#region Get / Set
/// <summary>
/// Gets the value of the property with the given <paramref name="key"/>.
/// If no property exists with that key, <paramref name="defaultValue"/> is added and returned.
/// </summary>
/// <param name="key">Key of the property to get.</param>
/// <param name="defaultValue">Default value to use is the property is not found.</param>
/// <param name="options">Options to use to retrieve the property.</param>
/// <returns>The value of the property.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
public T Get<T>(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);
}
/// <summary>
/// Gets the value of the property with the given <paramref name="key"/>.
/// If no property exists with that key, <paramref name="defaultValue"/> is added and returned.
/// </summary>
/// <param name="key">Key of the property to get.</param>
/// <param name="defaultValue">Default value to use is the property is not found.</param>
/// <returns>The value of the property.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
public T Get<T>(string key, T defaultValue)
{
return Get(key, defaultValue, PropertyOptions.None);
}
/// <summary>
/// Gets the value of the property with the given <paramref name="key"/>.
/// If no property exists with that key, an exception is thrown.
/// </summary>
/// <param name="key">Key of the property to get.</param>
/// <returns>The value of the property.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
/// <exception cref="KeyNotFoundException">Thrown if the <paramref name="key"/> doesn't exist in these properties.</exception>
public T Get<T>(string key)
{
Throw.IfNull(key, "key");
if (!m_properties.ContainsKey(key))
{
throw new KeyNotFoundException();
}
return GetImpl(key, default(T), PropertyOptions.None, false);
}
/// <summary>
/// Gets sub-properties with the given <paramref name="key"/>.
/// </summary>
/// <param name="key">Key to get the properties for.</param>
/// <returns>The sub-properties for the given <paramref name="key"/> or null if not found.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
IReadOnlyProperties IReadOnlyProperties.GetSubKey(string key)
{
return GetSubKey(key);
}
/// <summary>
/// Creates sub-properties with the given <paramref name="key"/> or opens existing one.
/// </summary>
/// <param name="key">Key to get the properties for.</param>
/// <param name="mode"></param>
/// <returns>The sub-properties for the given <paramref name="key"/> or null if could not create a key (in a read-only instance for example).</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
IReadOnlyProperties IReadOnlyProperties.CreateSubKey(string key, AccessContext mode)
{
return CreateSubKey(key, mode);
}
/// <summary>
/// Try gets the value of the property with the given <paramref name="key"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool TryGet<T>(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<T>(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;
}
/// <summary>
/// Sets the value of the property with the given <paramref name="key"/>.
/// </summary>
/// <param name="key">Key of the property to set.</param>
/// <param name="value">Value of the property.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="value"/> is null.</exception>
/// <exception cref="NotSupportedException">Thrown when <see cref="IsReadOnly"/> is true.</exception>
public void Set<T>(string key, T value)
{
Set(key, value, true);
}
private void Set<T>(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
/// <summary>
/// Creates sub-properties with the given <paramref name="key"/> or opens existing one.
/// </summary>
/// <param name="key">Key to get the properties for.</param>
/// <param name="mode"></param>
/// <returns>The sub-properties for the given <paramref name="key"/> or null if not found.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
public IProperties CreateSubKey(string key, AccessContext mode)
{
return GetSubKey(key, mode, true);
}
/// <summary>
/// Gets sub-properties with the given <paramref name="key"/>.
/// </summary>
/// <param name="key">Key to get the properties for.</param>
/// <returns>The sub-properties for the given <paramref name="key"/> or null if not found.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="key"/> contains spaces.</exception>
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;
}
/// <summary>
/// Attempts to remove the sub-key with the given <paramref name="key"/>.
/// </summary>
/// <param name="key">Name of the sub-key to remove.</param>
/// <param name="mode">PropertyChanged event is called when removed if the mode is in write.</param>
/// <returns>True if the sub-key existed and was removed. False if there is no sub-key with the name.</returns>
public bool RemoveSubKey(string key, AccessContext mode)
{
if (key == null)
{
return false;
}
return Remove(key, mode == AccessContext.Write);
}
#endregion
#region Other
/// <summary>
/// Returns a read-only version of these properties.
/// </summary>
/// <returns></returns>
public IReadOnlyProperties AsReadOnly()
{
return new ReadOnlyProperties(this);
}
/// <summary>
/// Whether the properties contain the specified value.
/// </summary>
/// <param name="key">Name of the property to check for.</param>
/// <returns>True if the value exists. False otherwise.</returns>
public bool Contains(string key)
{
return m_properties.ContainsKey(key);
}
/// <summary>
/// Attempts to remove the property with the given <paramref name="key"/>.
/// </summary>
/// <param name="key">Name of the property to remove.</param>
/// <returns>True if the property existed and was removed. False if there is no property with the name.</returns>
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;
}
/// <summary>
/// Clears all the properties and sub-keys. (Use with caution).
/// </summary>
public void Clear()
{
if (!OnPropertyChanging())
{
return;
}
int count = m_properties.Count;
m_properties.Clear();
if (count != 0)
{
OnPropertyChanged(new PropertyChangedEventArgs(this, null, null, null));
}
}
/// <summary>
/// Overriden.
/// </summary>
/// <returns></returns>
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<object>(value).ToString());
builder.Append(", ");
}
builder.Remove(builder.Length - 2, 2);
builder.Append("]");
return builder.ToString();
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return m_properties.Select(v => new KeyValuePair<string, object>(v.Key, v.Value)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// True if the content equals the other (does not implement equals to be sure it is still performant in a hash).
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool IsEqualTo(IReadOnlyProperties other)
{
Properties properties = other as Properties;
if (properties == null)
{
return false;
}
return m_properties.IsEquivalentTo(properties.m_properties, new ValueComparer());
}
/// <summary>
/// Adds the values to the target.
/// </summary>
public void CopyTo(IProperties other)
{
Throw.IfNull(other, "other");
foreach (var value in m_properties.ThreadSafeEnumerate)
{
other.Set(value.Key, CloneValue(value.Value, other));
}
}
/// <summary>
/// Try to save the file.
/// </summary>
/// <returns>Returns true if executed.</returns>
public virtual bool Save()
{
return false;
}
#endregion
#region Xml Serialization
public void WriteToXml(XmlSerializerWriter serializer)
{
WriteToXml(serializer, true);
}
/// <summary>
/// Writes to xml
/// </summary>
/// <param name="serializer"></param>
/// <param name="encapsulate">Whether to encapsulate writing into an element.</param>
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
/// <summary>
/// Clones the current properties.
/// </summary>
/// <remarks>
/// This cloning is shallow.
/// </remarks>
/// <returns></returns>
public virtual Properties Clone()
{
return new Properties(this, null);
}
/// <summary>
/// Clone
/// </summary>
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
/// <summary>
/// Starts an update that does not trigger the changed event
/// </summary>
/// <returns></returns>
public IDisposable BeginScopedUpdate()
{
m_isUpdatingCount++;
return new DisposableHelper(EndUpdate);
}
/// <summary>
/// Ends a scoped update
/// </summary>
public void EndUpdate()
{
Throw.InvalidOperationIf(!IsUpdating, "No update active!");
m_isUpdatingCount--;
if (!IsUpdating)
{
OnPropertyChanged(new PropertyChangedEventArgs(this, null, null, null));
}
}
#endregion
#region Events
/// <summary>
/// Raised before a property changes.
/// </summary>
/// <returns>False if cancelled (if the property cannot be changed)</returns>
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;
}
/// <summary>
/// Raised before a property changes.
/// </summary>
[field: NonSerialized]
public event EventHandler<CancelEventArgs> PropertyChanging;
/// <summary>
/// Triggered when the value of a property changes.
/// </summary>
[field: NonSerialized]
public event EventHandler<PropertyChangedEventArgs> PropertyChanged;
/// <summary>
/// Overridden. Don't raise event PropertyChanged when updating.
/// </summary>
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<ValueHolder>
{
bool IEqualityComparer<ValueHolder>.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<string, object>.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<string, ValueHolder>
{
public PropertyCollection()
: base(StringComparer.OrdinalIgnoreCase)
{}
protected override string GetKeyForItem(ValueHolder value)
{
return value.Key;
}
}
#endregion
}
}