blob: 0537ef1a1389b37ab0b7104d7949c903932bcbbf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Apache.Ignite.Core.Impl.Common
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Apache.Ignite.Core.Events;
using Apache.Ignite.Core.Impl.Binary;
using Apache.Ignite.Core.Impl.Events;
/// <summary>
/// Serializes and deserializes Ignite configurations to and from XML.
/// </summary>
internal static class IgniteConfigurationXmlSerializer
{
/** Attribute that specifies a type for abstract properties, such as IpFinder. */
private const string TypNameAttribute = "type";
/** Value for TypNameAttribute that denotes null. */
private const string TypNameNull = "null";
/** Xmlns. */
private const string XmlnsAttribute = "xmlns";
/** Xmlns. */
private const string KeyValPairElement = "pair";
/** Schema. */
private const string Schema = "http://ignite.apache.org/schema/dotnet/{0}Section";
/// <summary>
/// Deserializes configuration of specified type from XML string.
/// </summary>
/// <param name="xml">Xml string.</param>
/// <returns>Resulting configuration.</returns>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
[SuppressMessage("Microsoft.Usage", "CA2202: Do not call Dispose more than one time on an object")]
public static T Deserialize<T>(string xml) where T : new()
{
IgniteArgumentCheck.NotNullOrEmpty(xml, "xml");
using (var stringReader = new StringReader(xml))
using (var xmlReader = XmlReader.Create(stringReader))
{
// Skip XML header.
xmlReader.MoveToContent();
return Deserialize<T>(xmlReader);
}
}
/// <summary>
/// Deserializes configuration of specified type from specified <see cref="XmlReader"/>.
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>Resulting configuration.</returns>
public static T Deserialize<T>(XmlReader reader) where T : new()
{
IgniteArgumentCheck.NotNull(reader, "reader");
var cfg = new T();
if (reader.NodeType == XmlNodeType.Element || reader.Read())
ReadElement(reader, cfg, new TypeResolver());
return cfg;
}
/// <summary>
/// Serializes specified configuration to <see cref="XmlWriter" />.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="writer">The writer.</param>
/// <param name="rootElementName">Name of the root element.</param>
public static void Serialize(object configuration, XmlWriter writer, string rootElementName)
{
IgniteArgumentCheck.NotNull(configuration, "configuration");
IgniteArgumentCheck.NotNull(writer, "writer");
IgniteArgumentCheck.NotNullOrEmpty(rootElementName, "rootElementName");
WriteElement(configuration, writer, rootElementName, configuration.GetType(), writeSchema: true);
}
/// <summary>
/// Serializes specified configuration to <see cref="XmlWriter" />.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="rootElementName">Name of the root element.</param>
/// <returns>XML string.</returns>
public static string Serialize(object configuration, string rootElementName)
{
var sb = new StringBuilder();
var settings = new XmlWriterSettings
{
Indent = true
};
using (var xmlWriter = XmlWriter.Create(sb, settings))
{
Serialize(configuration, xmlWriter, rootElementName);
}
return sb.ToString();
}
/// <summary>
/// Writes new element.
/// </summary>
private static void WriteElement(object obj, XmlWriter writer, string rootElementName, Type valueType,
PropertyInfo property = null, bool writeSchema = false)
{
if (property != null)
{
if (!property.CanWrite && !IsKeyValuePair(property.DeclaringType))
return;
if (IsIgnored(property))
return;
}
if (writeSchema)
{
// Write xmlns for the root element.
writer.WriteStartElement(rootElementName, string.Format(Schema, valueType.Name));
}
else
{
writer.WriteStartElement(rootElementName);
}
if (IsBasicType(valueType))
WriteBasicProperty(obj, writer, valueType, property);
else if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof (ICollection<>))
WriteCollectionProperty(obj, writer, valueType, property);
else if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof (IDictionary<,>))
WriteDictionaryProperty(obj, writer, valueType, property);
else
WriteComplexProperty(obj, writer, valueType);
writer.WriteEndElement();
}
/// <summary>
/// Writes the property of a basic type (primitives, strings, types).
/// </summary>
private static void WriteBasicProperty(object obj, XmlWriter writer, Type valueType, PropertyInfo property)
{
var converter = GetConverter(property, valueType);
var stringValue = converter.ConvertToInvariantString(obj);
writer.WriteString(stringValue ?? "");
}
/// <summary>
/// Writes the collection property.
/// </summary>
private static void WriteCollectionProperty(object obj, XmlWriter writer, Type valueType, PropertyInfo property)
{
var elementType = valueType.GetGenericArguments().Single();
var elementTypeName = PropertyNameToXmlName(elementType.Name);
foreach (var element in (IEnumerable)obj)
WriteElement(element, writer, elementTypeName, elementType, property);
}
/// <summary>
/// Writes the dictionary property.
/// </summary>
private static void WriteDictionaryProperty(object obj, XmlWriter writer, Type valueType, PropertyInfo property)
{
var elementType = typeof (KeyValuePair<,>).MakeGenericType(valueType.GetGenericArguments());
foreach (var element in (IEnumerable)obj)
WriteElement(element, writer, KeyValPairElement, elementType, property);
}
/// <summary>
/// Writes the complex property (nested object).
/// </summary>
private static void WriteComplexProperty(object obj, XmlWriter writer, Type valueType)
{
if (obj == null)
{
// Happens with reference-type properties that have non-null default value.
// Example: IgniteClientConfiguration.Logger
writer.WriteAttributeString(TypNameAttribute, TypNameNull);
return;
}
var props = GetNonDefaultProperties(obj).OrderBy(x => x.Name).ToList();
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) && !IsIgnored(p)))
{
var converter = GetConverter(prop, prop.PropertyType);
var stringValue = converter.ConvertToInvariantString(prop.GetValue(obj, null));
writer.WriteAttributeString(PropertyNameToXmlName(prop.Name), stringValue ?? "");
}
// Write elements
foreach (var prop in props.Where(p => !IsBasicType(p.PropertyType)))
WriteElement(prop.GetValue(obj, null), writer, PropertyNameToXmlName(prop.Name),
prop.PropertyType, prop);
}
/// <summary>
/// Reads the element.
/// </summary>
private static void ReadElement(XmlReader reader, object target, TypeResolver resolver)
{
var targetType = target.GetType();
// Read attributes
while (reader.MoveToNextAttribute())
{
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);
prop.SetValue(target, value, null);
}
// Read content
reader.MoveToElement();
while (reader.Read())
{
if (reader.NodeType != XmlNodeType.Element)
continue;
var prop = GetPropertyOrThrow(reader.Name, reader.Value, targetType);
var value = ReadPropertyValue(reader, resolver, prop, targetType);
prop.SetValue(target, value, null);
}
}
/// <summary>
/// Reads the property value.
/// </summary>
private static object ReadPropertyValue(XmlReader reader, TypeResolver resolver,
PropertyInfo prop, Type targetType)
{
var propType = prop.PropertyType;
if (propType == typeof(object))
{
propType = ResolvePropertyType(reader, propType, prop.Name, targetType, resolver);
}
if (propType == null)
{
return null;
}
if (IsBasicType(propType))
{
// Regular property in xmlElement form.
return ConvertBasicValue(reader.ReadString(), prop, propType);
}
if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
// Collection.
return ReadCollectionProperty(reader, prop, targetType, resolver);
}
if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
// Dictionary.
return ReadDictionaryProperty(reader, prop, resolver);
}
// Nested object (complex property).
return ReadComplexProperty(reader, propType, prop.Name, targetType, resolver);
}
/// <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);
if (propType == null)
{
return null;
}
var nestedVal = Activator.CreateInstance(propType);
using (var subReader = reader.ReadSubtree())
{
subReader.Read(); // read first element
ReadElement(subReader, nestedVal, resolver);
}
return nestedVal;
}
/// <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 (typeName == TypNameNull)
return null;
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 IList ReadCollectionProperty(XmlReader reader, PropertyInfo prop, Type targetType,
TypeResolver resolver)
{
var elementType = prop.PropertyType.GetGenericArguments().Single();
var listType = typeof (List<>).MakeGenericType(elementType);
var list = (IList) Activator.CreateInstance(listType);
var converter = IsBasicType(elementType) ? GetConverter(prop, elementType) : null;
using (var subReader = reader.ReadSubtree())
{
subReader.Read(); // skip list head
while (subReader.Read())
{
if (subReader.NodeType != XmlNodeType.Element)
continue;
if (subReader.Name != PropertyNameToXmlName(elementType.Name))
throw new ConfigurationErrorsException(
string.Format("Invalid list element in IgniteConfiguration: expected '{0}', but was '{1}'",
PropertyNameToXmlName(elementType.Name), subReader.Name));
list.Add(converter != null
? converter.ConvertFromInvariantString(subReader.ReadString())
: ReadComplexProperty(subReader, elementType, prop.Name, targetType, resolver));
}
}
return list;
}
/// <summary>
/// Reads the dictionary.
/// </summary>
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())
{
subReader.Read(); // skip list head
while (subReader.Read())
{
if (subReader.NodeType != XmlNodeType.Element)
continue;
if (subReader.Name != PropertyNameToXmlName(KeyValPairElement))
throw new ConfigurationErrorsException(
string.Format("Invalid dictionary element in IgniteConfiguration: expected '{0}', " +
"but was '{1}'", KeyValPairElement, subReader.Name));
var pair = (IPair) Activator.CreateInstance(pairType);
var pairReader = subReader.ReadSubtree();
pairReader.Read();
ReadElement(pairReader, pair, resolver);
dict[pair.Key] = pair.Value;
}
}
return dict;
}
/// <summary>
/// Reads the basic value.
/// </summary>
private static object ConvertBasicValue(string propVal, PropertyInfo property, Type propertyType)
{
var converter = GetConverter(property, propertyType);
return converter.ConvertFromInvariantString(propVal);
}
/// <summary>
/// Gets concrete derived types.
/// </summary>
private static List<Type> GetConcreteDerivedTypes(Type type)
{
return TypeResolver.GetAssemblyTypesSafe(typeof(IIgnite).Assembly)
.Where(t => t.IsClass && !t.IsAbstract && type.IsAssignableFrom(t)).ToList();
}
/// <summary>
/// Gets specified property from a type or throws an exception.
/// </summary>
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;
}
/// <summary>
/// Converts an XML name to CLR name.
/// </summary>
private static string XmlNameToPropertyName(string name)
{
Debug.Assert(name.Length > 0);
if (name == "int")
return "Int32"; // allow aliases
return char.ToUpperInvariant(name[0]) + name.Substring(1);
}
/// <summary>
/// Converts a CLR name to XML name.
/// </summary>
private static string PropertyNameToXmlName(string name)
{
Debug.Assert(name.Length > 0);
if (name == "Int32")
return "int"; // allow aliases
return char.ToLowerInvariant(name[0]) + name.Substring(1);
}
/// <summary>
/// Determines whether specified type is a basic built-in type.
/// </summary>
private static bool IsBasicType(Type propertyType)
{
Debug.Assert(propertyType != null);
if (IsKeyValuePair(propertyType))
return false;
return propertyType.IsValueType || propertyType == typeof (string) || propertyType == typeof (Type);
}
/// <summary>
/// Determines whether specified type is KeyValuePair.
/// </summary>
private static bool IsKeyValuePair(Type propertyType)
{
Debug.Assert(propertyType != null);
return propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof (KeyValuePair<,>);
}
/// <summary>
/// Gets converter for a property.
/// </summary>
private static TypeConverter GetConverter(PropertyInfo property, Type propertyType)
{
Debug.Assert(propertyType != null);
if (propertyType.IsEnum)
return new GenericEnumConverter(propertyType);
if (propertyType == typeof (Type))
return TypeStringConverter.Instance;
if (propertyType == typeof(bool))
return BooleanLowerCaseConverter.Instance;
if (property != null &&
property.DeclaringType == typeof (IgniteConfiguration) && property.Name == "IncludedEventTypes")
return EventTypeConverter.Instance;
if (property != null &&
property.DeclaringType == typeof (LocalEventListener) && property.Name == "EventTypes")
return EventTypeConverter.Instance;
if (propertyType == typeof (object))
return ObjectStringConverter.Instance;
var converter = TypeDescriptor.GetConverter(propertyType);
if (converter == null || !converter.CanConvertFrom(typeof(string)) ||
!converter.CanConvertTo(typeof(string)))
throw new ConfigurationErrorsException("No converter for type " + propertyType);
return converter;
}
/// <summary>
/// Gets properties with non-default value.
/// </summary>
private static IEnumerable<PropertyInfo> GetNonDefaultProperties(object obj)
{
if (obj == null)
{
Debug.Assert(obj != null);
}
return obj.GetType().GetProperties()
.Where(p => p.GetIndexParameters().Length == 0 && // Skip indexed properties.
!Equals(p.GetValue(obj, null), GetDefaultValue(p)));
}
/// <summary>
/// Gets the default value for a property.
/// </summary>
private static object GetDefaultValue(PropertyInfo property)
{
var attr = property.GetCustomAttributes(true).OfType<DefaultValueAttribute>().FirstOrDefault();
if (attr != null)
{
return attr.Value;
}
var declType = property.DeclaringType;
if (declType != null && !declType.IsAbstract && declType.GetConstructor(new Type[0]) != null)
{
return property.GetValue(Activator.CreateInstance(declType), null);
}
var propertyType = property.PropertyType;
if (propertyType.IsValueType)
{
return Activator.CreateInstance(propertyType);
}
return null;
}
/// <summary>
/// Determines whether the specified property is marked with XmlIgnore.
/// </summary>
private static bool IsIgnored(PropertyInfo property)
{
Debug.Assert(property != null);
return property.GetCustomAttributes(typeof(XmlIgnoreAttribute), 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; }
}
}
}
}