IGNITE-4846 .NET: Support complex type dictionaries in app.config configuration
This closes #1653 Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/3b89a5ce Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/3b89a5ce Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/3b89a5ce Branch: refs/heads/ignite-4565-ddl Commit: 3b89a5ce78fadf4e39c8c927afd8ffef7b9fa186 Parents: b4cc8a7 Author: Pavel Tupitsyn <[email protected]> Authored: Tue Mar 21 18:08:03 2017 +0300 Committer: Pavel Tupitsyn <[email protected]> Committed: Tue Mar 21 18:08:03 2017 +0300 ---------------------------------------------------------------------- .../IgniteConfigurationSerializerTest.cs | 44 +++- .../Common/IgniteConfigurationXmlSerializer.cs | 250 ++++++++++++------- 2 files changed, 204 insertions(+), 90 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/3b89a5ce/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs index 2f9366e..4335d11 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs @@ -126,7 +126,10 @@ namespace Apache.Ignite.Core.Tests <int>TaskFailed</int> <int>JobFinished</int> </includedEventTypes> - <userAttributes><pair key='myNode' value='true' /></userAttributes> + <userAttributes> + <pair key='myNode' value='true' /> + <pair key='foo'><value type='Apache.Ignite.Core.Tests.IgniteConfigurationSerializerTest+FooClass, Apache.Ignite.Core.Tests'><bar>Baz</bar></value></pair> + </userAttributes> <atomicConfiguration backups='2' cacheMode='Local' atomicSequenceReserveSize='250' /> <transactionConfiguration defaultTransactionConcurrency='Optimistic' defaultTransactionIsolation='RepeatableRead' defaultTimeout='0:1:2' pessimisticTransactionLogSize='15' pessimisticTransactionLogLinger='0:0:33' /> <logger type='Apache.Ignite.Core.Tests.IgniteConfigurationSerializerTest+TestLogger, Apache.Ignite.Core.Tests' /> @@ -202,7 +205,11 @@ namespace Apache.Ignite.Core.Tests Assert.AreEqual(99, af.Partitions); Assert.IsTrue(af.ExcludeNeighbors); - Assert.AreEqual(new Dictionary<string, object> {{"myNode", "true"}}, cfg.UserAttributes); + Assert.AreEqual(new Dictionary<string, object> + { + {"myNode", "true"}, + {"foo", new FooClass {Bar = "Baz"}} + }, cfg.UserAttributes); var atomicCfg = cfg.AtomicConfiguration; Assert.AreEqual(2, atomicCfg.Backups); @@ -544,8 +551,9 @@ namespace Apache.Ignite.Core.Tests Assert.IsNull(xVal); Assert.IsNull(yVal); } - else if (propType != typeof(string) && propType.IsGenericType - && propType.GetGenericTypeDefinition() == typeof (ICollection<>)) + else if (propType != typeof(string) && propType.IsGenericType && + (propType.GetGenericTypeDefinition() == typeof(ICollection<>) || + propType.GetGenericTypeDefinition() == typeof(IDictionary<,>) )) { var xCol = ((IEnumerable) xVal).OfType<object>().ToList(); var yCol = ((IEnumerable) yVal).OfType<object>().ToList(); @@ -738,7 +746,8 @@ namespace Apache.Ignite.Core.Tests SuppressWarnings = true, WorkDirectory = @"c:\work", IsDaemon = true, - UserAttributes = Enumerable.Range(1, 10).ToDictionary(x => x.ToString(), x => (object) x), + UserAttributes = Enumerable.Range(1, 10).ToDictionary(x => x.ToString(), + x => x%2 == 0 ? (object) x : new FooClass {Bar = x.ToString()}), AtomicConfiguration = new AtomicConfiguration { CacheMode = CacheMode.Replicated, @@ -904,7 +913,30 @@ namespace Apache.Ignite.Core.Tests /// </summary> public class FooClass { - // No-op. + public string Bar { get; set; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return string.Equals(Bar, ((FooClass) obj).Bar); + } + + public override int GetHashCode() + { + return Bar != null ? Bar.GetHashCode() : 0; + } + + public static bool operator ==(FooClass left, FooClass right) + { + return Equals(left, right); + } + + public static bool operator !=(FooClass left, FooClass right) + { + return !Equals(left, right); + } } /// <summary> http://git-wip-us.apache.org/repos/asf/ignite/blob/3b89a5ce/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/IgniteConfigurationXmlSerializer.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/IgniteConfigurationXmlSerializer.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/IgniteConfigurationXmlSerializer.cs index feb0f9e..6c5d620 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/IgniteConfigurationXmlSerializer.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/IgniteConfigurationXmlSerializer.cs @@ -153,9 +153,19 @@ namespace Apache.Ignite.Core.Impl.Common { var props = GetNonDefaultProperties(obj).OrderBy(x => x.Name).ToList(); - // Specify type for interfaces and abstract classes - if (valueType.IsAbstract) + var realType = obj.GetType(); + + // Specify type when it differs from declared type. + if (valueType != realType) + { writer.WriteAttributeString(TypNameAttribute, TypeStringConverter.Convert(obj.GetType())); + } + + if (IsBasicType(obj.GetType())) + { + WriteBasicProperty(obj, writer, realType, null); + return; + } // Write attributes foreach (var prop in props.Where(p => IsBasicType(p.PropertyType) && !IsObsolete(p))) @@ -181,10 +191,14 @@ namespace Apache.Ignite.Core.Impl.Common // Read attributes while (reader.MoveToNextAttribute()) { - var name = reader.Name; - var val = reader.Value; + if (reader.Name == TypNameAttribute || reader.Name == XmlnsAttribute) + continue; + + var prop = GetPropertyOrThrow(reader.Name, reader.Value, target.GetType()); + + var value = ConvertBasicValue(reader.Value, prop, prop.PropertyType); - SetProperty(target, name, val); + prop.SetValue(target, value, null); } // Read content @@ -195,64 +209,56 @@ namespace Apache.Ignite.Core.Impl.Common if (reader.NodeType != XmlNodeType.Element) continue; - var name = reader.Name; - var prop = GetPropertyOrThrow(name, reader.Value, targetType); - var propType = prop.PropertyType; + var prop = GetPropertyOrThrow(reader.Name, reader.Value, targetType); - if (IsBasicType(propType)) - { - // Regular property in xmlElement form - SetProperty(target, name, reader.ReadString()); - } - else if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof (ICollection<>)) - { - // Collection - ReadCollectionProperty(reader, prop, target, resolver); - } - else if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof (IDictionary<,>)) - { - // Dictionary - ReadDictionaryProperty(reader, prop, target); - } - else - { - // Nested object (complex property) - prop.SetValue(target, ReadComplexProperty(reader, propType, prop.Name, targetType, resolver), null); - } + var value = ReadPropertyValue(reader, resolver, prop, targetType); + + prop.SetValue(target, value, null); } } /// <summary> - /// Reads the complex property (nested object). + /// Reads the property value. /// </summary> - private static object ReadComplexProperty(XmlReader reader, Type propType, string propName, Type targetType, - TypeResolver resolver) + private static object ReadPropertyValue(XmlReader reader, TypeResolver resolver, + PropertyInfo prop, Type targetType) { - if (propType.IsAbstract) + var propType = prop.PropertyType; + + if (propType == typeof(object)) { - var typeName = reader.GetAttribute(TypNameAttribute); + propType = ResolvePropertyType(reader, propType, prop.Name, targetType, resolver); + } - var derivedTypes = GetConcreteDerivedTypes(propType); + if (IsBasicType(propType)) + { + // Regular property in xmlElement form. + return ConvertBasicValue(reader.ReadString(), prop, propType); + } - propType = typeName == null - ? null - : resolver.ResolveType(typeName) ?? derivedTypes.FirstOrDefault(x => x.Name == typeName); + if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(ICollection<>)) + { + // Collection. + return ReadCollectionProperty(reader, prop, targetType, resolver); + } - if (propType == null) - { - var message = string.Format("'type' attribute is required for '{0}.{1}' property", targetType.Name, - propName); + if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + { + // Dictionary. + return ReadDictionaryProperty(reader, prop, resolver); + } - if (typeName != null) - { - message += ", specified type cannot be resolved: " + typeName; - } - else if (derivedTypes.Any()) - message += ", possible values are: " + string.Join(", ", derivedTypes.Select(x => x.Name)); + // Nested object (complex property). + return ReadComplexProperty(reader, propType, prop.Name, targetType, resolver); + } - throw new ConfigurationErrorsException(message); - } - } + /// <summary> + /// Reads the complex property (nested object). + /// </summary> + private static object ReadComplexProperty(XmlReader reader, Type propType, string propName, Type targetType, + TypeResolver resolver) + { + propType = ResolvePropertyType(reader, propType, propName, targetType, resolver); var nestedVal = Activator.CreateInstance(propType); @@ -267,9 +273,46 @@ namespace Apache.Ignite.Core.Impl.Common } /// <summary> + /// Resolves the type of the property. + /// </summary> + private static Type ResolvePropertyType(XmlReader reader, Type propType, string propName, Type targetType, + TypeResolver resolver) + { + var typeName = reader.GetAttribute(TypNameAttribute); + + if (!propType.IsAbstract && typeName == null) + return propType; + + var res = typeName == null + ? null + : resolver.ResolveType(typeName) ?? + GetConcreteDerivedTypes(propType).FirstOrDefault(x => x.Name == typeName); + + if (res != null) + return res; + + var message = string.Format("'type' attribute is required for '{0}.{1}' property", targetType.Name, + propName); + + var derivedTypes = GetConcreteDerivedTypes(propType); + + + if (typeName != null) + { + message += ", specified type cannot be resolved: " + typeName; + } + else if (derivedTypes.Any()) + { + message += ", possible values are: " + string.Join(", ", derivedTypes.Select(x => x.Name)); + } + + throw new ConfigurationErrorsException(message); + } + + /// <summary> /// Reads the collection. /// </summary> - private static void ReadCollectionProperty(XmlReader reader, PropertyInfo prop, object target, + private static IList ReadCollectionProperty(XmlReader reader, PropertyInfo prop, Type targetType, TypeResolver resolver) { var elementType = prop.PropertyType.GetGenericArguments().Single(); @@ -295,22 +338,24 @@ namespace Apache.Ignite.Core.Impl.Common list.Add(converter != null ? converter.ConvertFromInvariantString(subReader.ReadString()) - : ReadComplexProperty(subReader, elementType, prop.Name, target.GetType(), resolver)); + : ReadComplexProperty(subReader, elementType, prop.Name, targetType, resolver)); } } - prop.SetValue(target, list, null); + return list; } /// <summary> /// Reads the dictionary. /// </summary> - private static void ReadDictionaryProperty(XmlReader reader, PropertyInfo prop, object target) + private static IDictionary ReadDictionaryProperty(XmlReader reader, PropertyInfo prop, TypeResolver resolver) { var keyValTypes = prop.PropertyType.GetGenericArguments(); var dictType = typeof (Dictionary<,>).MakeGenericType(keyValTypes); + var pairType = typeof(Pair<,>).MakeGenericType(keyValTypes); + var dict = (IDictionary) Activator.CreateInstance(dictType); using (var subReader = reader.ReadSubtree()) @@ -326,42 +371,29 @@ namespace Apache.Ignite.Core.Impl.Common string.Format("Invalid dictionary element in IgniteConfiguration: expected '{0}', " + "but was '{1}'", KeyValPairElement, subReader.Name)); - var key = subReader.GetAttribute("key"); + var pair = (IPair) Activator.CreateInstance(pairType); - if (key == null) - throw new ConfigurationErrorsException( - "Invalid dictionary entry, key attribute is missing for property " + prop); + var pairReader = subReader.ReadSubtree(); - dict[key] = subReader.GetAttribute("value"); + pairReader.Read(); + + ReadElement(pairReader, pair, resolver); + + dict[pair.Key] = pair.Value; } } - prop.SetValue(target, dict, null); + return dict; } /// <summary> - /// Sets the property. + /// Reads the basic value. /// </summary> - private static void SetProperty(object target, string propName, string propVal) + private static object ConvertBasicValue(string propVal, PropertyInfo property, Type propertyType) { - if (propName == TypNameAttribute || propName == XmlnsAttribute) - return; + var converter = GetConverter(property, propertyType); - var type = target.GetType(); - var property = GetPropertyOrThrow(propName, propVal, type); - - if (!property.CanWrite) - { - throw new ConfigurationErrorsException(string.Format( - "Invalid IgniteConfiguration attribute '{0}={1}', property '{2}.{3}' is not writeable", - propName, propVal, type, property.Name)); - } - - var converter = GetConverter(property, property.PropertyType); - - var convertedVal = converter.ConvertFromInvariantString(propVal); - - property.SetValue(target, convertedVal, null); + return converter.ConvertFromInvariantString(propVal); } /// <summary> @@ -376,15 +408,24 @@ namespace Apache.Ignite.Core.Impl.Common /// <summary> /// Gets specified property from a type or throws an exception. /// </summary> - private static PropertyInfo GetPropertyOrThrow(string propName, string propVal, Type type) + private static PropertyInfo GetPropertyOrThrow(string propName, object propVal, Type type) { var property = type.GetProperty(XmlNameToPropertyName(propName)); if (property == null) + { throw new ConfigurationErrorsException( string.Format( "Invalid IgniteConfiguration attribute '{0}={1}', there is no such property on '{2}'", propName, propVal, type)); + } + + if (!property.CanWrite) + { + throw new ConfigurationErrorsException(string.Format( + "Invalid IgniteConfiguration attribute '{0}={1}', property '{2}.{3}' is not writeable", + propName, propVal, type, property.Name)); + } return property; } @@ -425,8 +466,7 @@ namespace Apache.Ignite.Core.Impl.Common if (IsKeyValuePair(propertyType)) return false; - return propertyType.IsValueType || propertyType == typeof (string) || propertyType == typeof (Type) || - propertyType == typeof (object); + return propertyType.IsValueType || propertyType == typeof (string) || propertyType == typeof (Type); } /// <summary> @@ -444,7 +484,6 @@ namespace Apache.Ignite.Core.Impl.Common /// </summary> private static TypeConverter GetConverter(PropertyInfo property, Type propertyType) { - Debug.Assert(property != null); Debug.Assert(propertyType != null); if (propertyType.IsEnum) @@ -456,7 +495,8 @@ namespace Apache.Ignite.Core.Impl.Common if (propertyType == typeof(bool)) return BooleanLowerCaseConverter.Instance; - if (property.DeclaringType == typeof (IgniteConfiguration) && property.Name == "IncludedEventTypes") + if (property != null && + property.DeclaringType == typeof (IgniteConfiguration) && property.Name == "IncludedEventTypes") return EventTypeConverter.Instance; if (propertyType == typeof (object)) @@ -508,5 +548,47 @@ namespace Apache.Ignite.Core.Impl.Common return property.GetCustomAttributes(typeof(ObsoleteAttribute), true).Any(); } + + /// <summary> + /// Non-generic Pair accessor. + /// </summary> + private interface IPair + { + /// <summary> + /// Gets the key. + /// </summary> + object Key { get; } + + /// <summary> + /// Gets the value. + /// </summary> + object Value { get; } + } + + /// <summary> + /// Surrogate dictionary entry to overcome immutable KeyValuePair. + /// </summary> + private class Pair<TK, TV> : IPair + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + // ReSharper disable once MemberCanBePrivate.Local + public TK Key { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + // ReSharper disable once MemberCanBePrivate.Local + public TV Value { get; set; } + + /** <inheritdoc /> */ + object IPair.Key + { + get { return Key; } + } + + /** <inheritdoc /> */ + object IPair.Value + { + get { return Value; } + } + } } }
