// --------------------------------------------------------------------------------------------------------------------
// <copyright file="RedirectDeserializationBinder.cs" company="Catel development team">
// Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
// <see cref="SerializationBinder" /> class that supports backwards compatible serialization.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using Catel.Properties;
using Catel.Reflection;
using log4net;
namespace Catel.Runtime.Serialization
{
/// <summary>
/// <see cref="SerializationBinder"/> class that supports backwards compatible serialization.
/// </summary>
internal sealed class RedirectDeserializationBinder : SerializationBinder
{
#region Variables
/// <summary>
/// The <see cref="ILog">log</see> object.
/// </summary>
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// A dictionary of all <see cref="RedirectTypeAttribute"/> found.
/// </summary>
private readonly Dictionary<string, RedirectTypeAttribute> _redirectAttributes = new Dictionary<string, RedirectTypeAttribute>();
/// <summary>
/// A list of microsoft public key tokens.
/// </summary>
private static List<string> _microsoftPublicKeyTokens;
#endregion
#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
/// <para />
/// Creates a custom binder that redirects all the types to new types if required. All properties
/// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
/// </summary>
/// <remarks>
/// This constructor searches for attributes of the current application domain.
/// </remarks>
public RedirectDeserializationBinder()
: this(AppDomain.CurrentDomain) { }
/// <summary>
/// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
/// <para />
/// Creates a custom binder that redirects all the types to new types if required. All properties
/// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
/// </summary>
/// <param name="appDomain"><see cref="AppDomain"/> to search in.</param>
/// <remarks>
/// This constructor searches for attributes in a specific application domain.
/// </remarks>
public RedirectDeserializationBinder(AppDomain appDomain)
{
Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);
List<Assembly> assemblies = new List<Assembly>();
foreach (Assembly assembly in AssemblyHelper.GetLoadedAssemblies(appDomain))
{
// Skip dynamic assemblies
if (string.Compare(assembly.FullName, "Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null") != 0)
{
// Skip Microsoft assemblies
string company = assembly.Company();
if (!company.Contains("Microsoft"))
{
assemblies.Add(assembly);
}
}
}
Initialize(assemblies);
Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
/// <para />
/// This method automatically loads all the assemblies into the current AppDomain and then uses the
/// <see cref="RedirectDeserializationBinder(IEnumerable{Assembly})"/> constructor.
/// <para />
/// Creates a custom binder that redirects all the types to new types if required. All properties
/// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
/// </summary>
/// <param name="assemblies">Array of assembly locations that should be searched.</param>
/// <remarks>
/// This constructor searches for attributes in specific assemblies.
/// <para />
/// No exceptions are caught during the loading of the assemblies. The caller is responsible for handling
/// the exceptions correctly.
/// </remarks>
public RedirectDeserializationBinder(IEnumerable<string> assemblies)
{
Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);
List<Assembly> loadedAssemblies = new List<Assembly>();
foreach (string assembly in assemblies)
{
loadedAssemblies.Add(Assembly.Load(assembly));
}
Initialize(loadedAssemblies);
Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
/// <para />
/// Creates a custom binder that redirects all the types to new types if required. All properties
/// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
/// </summary>
/// <param name="assemblies">Array of assembly locations that should be searched.</param>
/// <remarks>
/// This constructor searches for attributes in specific assemblies.
/// </remarks>
public RedirectDeserializationBinder(IEnumerable<Assembly> assemblies)
{
Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);
Initialize(assemblies);
Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
}
#endregion
#region Properties
/// <summary>
/// Gets the Microsoft public key tokens.
/// </summary>
/// <value>The Microsoft public key tokens.</value>
private static IEnumerable<string> MicrosoftPublicKeyTokens
{
get
{
if (_microsoftPublicKeyTokens == null)
{
_microsoftPublicKeyTokens = new List<string>();
_microsoftPublicKeyTokens.Add("b77a5c561934e089");
_microsoftPublicKeyTokens.Add("b03f5f7f11d50a3a");
_microsoftPublicKeyTokens.Add("31bf3856ad364e35");
}
return _microsoftPublicKeyTokens;
}
}
#endregion
#region Methods
/// <summary>
/// Initializes the binder by searching for all <see cref="RedirectTypeAttribute"/> in the
/// assemblies passed to this method.
/// </summary>
/// <param name="assemblies">Array of assembly locations that should be searched.</param>
private void Initialize(IEnumerable<Assembly> assemblies)
{
Log.Debug(TraceMessages.InitializingRedirectDeserializationBinder);
Type attributeType = typeof(RedirectTypeAttribute);
foreach (Assembly assembly in assemblies)
{
try
{
foreach (Type type in assembly.GetTypes())
{
InitializeAttributes(type, (RedirectTypeAttribute[])type.GetCustomAttributes(attributeType, true));
foreach (MemberInfo member in type.GetMembers())
{
InitializeAttributes(member, (RedirectTypeAttribute[])member.GetCustomAttributes(attributeType, true));
}
}
}
catch (Exception ex)
{
Log.Warn(ex, TraceMessages.FailedToGetTypesOfAssembly, assembly.FullName);
}
}
Log.Debug(TraceMessages.InitializedRedirectDeserializationBinder);
}
/// <summary>
/// Initializes the binder by searching for all <see cref="RedirectTypeAttribute"/> in the
/// attributes passed to this method.
/// </summary>
/// <param name="decoratedObject">object that was decorated with the attribute.</param>
/// <param name="attributes">Array of attributes to search for.</param>
private void InitializeAttributes(object decoratedObject, RedirectTypeAttribute[] attributes)
{
if (decoratedObject == null)
{
return;
}
if (attributes.Length == 0)
{
return;
}
Type type = null;
if (decoratedObject is Type)
{
type = decoratedObject as Type;
}
foreach (RedirectTypeAttribute attribute in attributes)
{
if (type != null)
{
string typeName = TypeHelper.FormatType(type.Assembly.FullName, type.FullName);
typeName = ConvertTypeToVersionIndependentType(typeName);
string finalTypeName, finalAssemblyName;
SplitType(typeName, out finalAssemblyName, out finalTypeName);
attribute.NewTypeName = finalTypeName;
attribute.NewAssemblyName = finalAssemblyName;
}
if (_redirectAttributes.ContainsKey(attribute.OriginalType))
{
Log.Warn(TraceMessages.RedirectAlreadyAdded, attribute.OriginalType, _redirectAttributes[attribute.OriginalType].TypeToLoad, attribute.TypeToLoad);
}
else
{
Log.Debug(TraceMessages.AddingRedirectFromTypeToType, attribute.OriginalTypeName, attribute.NewTypeName);
_redirectAttributes.Add(attribute.OriginalType, attribute);
}
}
}
/// <summary>
/// Binds an assembly and typename to a specific type.
/// </summary>
/// <param name="assemblyName">Original assembly name.</param>
/// <param name="typeName">Original type name.</param>
/// <returns><see cref="Type"/> that the serialization should actually use.</returns>
public override Type BindToType(string assemblyName, string typeName)
{
string currentType = TypeHelper.FormatType(assemblyName, typeName);
string currentTypeVersionIndependent = ConvertTypeToVersionIndependentType(currentType);
string newType = ConvertTypeToNewType(currentTypeVersionIndependent);
Type typeToDeserialize = LoadType(newType) ?? (LoadType(currentTypeVersionIndependent) ?? LoadType(currentType));
if (typeToDeserialize == null)
{
Log.Error(Resources.CouldNotLoadNewType, currentType, newType);
}
return typeToDeserialize;
}
/// <summary>
/// Tries to load a type on a safe way.
/// </summary>
/// <param name="type">The type to load.</param>
/// <returns>The type or <c>null</c> if this method fails.</returns>
/// <remarks>
/// In case this method fails to load the type, a warning will be traced with additional information.
/// </remarks>
private static Type LoadType(string type)
{
Type loadedType = null;
try
{
loadedType = Type.GetType(type);
}
catch (Exception ex)
{
Log.Warn(ex, TraceMessages.FailedToLoadType, type);
}
return loadedType;
}
/// <summary>
/// Splits the type into a type name and assembly name.
/// </summary>
/// <param name="type">Type to split.</param>
/// <param name="assemblyName">Assemby name.</param>
/// <param name="typeName">Type name.</param>
private static void SplitType(string type, out string assemblyName, out string typeName)
{
int splitterPos = type.IndexOf(", ");
typeName = (splitterPos != -1) ? type.Substring(0, splitterPos).Trim() : type;
assemblyName = (splitterPos != -1) ? type.Substring(splitterPos + 1).Trim() : type;
}
/// <summary>
/// Converts a string representation of a type to a version independent type by removing the assembly version information.
/// </summary>
/// <param name="type">Type to convert.</param>
/// <returns>String representing the type without version information.</returns>
private static string ConvertTypeToVersionIndependentType(string type)
{
const string InnerTypesEnd = ",";
string newType = type;
string[] innerTypes = GetInnerTypes(newType);
if (innerTypes.Length > 0)
{
// Remove inner types
newType = newType.Replace(string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)), string.Empty);
for (int i = 0; i < innerTypes.Length; i++)
{
innerTypes[i] = ConvertTypeToVersionIndependentType(innerTypes[i]);
}
}
string typeName, assemblyName;
SplitType(newType, out assemblyName, out typeName);
bool isMicrosoftAssembly = false;
foreach (string t in MicrosoftPublicKeyTokens)
{
if (assemblyName.Contains(t))
{
isMicrosoftAssembly = true;
break;
}
}
// Remove version info from assembly (if not signed by Microsoft)
if (!isMicrosoftAssembly)
{
int splitterPos = assemblyName.IndexOf(", ");
if (splitterPos != -1)
{
assemblyName = assemblyName.Substring(0, splitterPos);
}
}
newType = TypeHelper.FormatType(assemblyName, typeName);
if (innerTypes.Length > 0)
{
int innerTypesIndex = newType.IndexOf(InnerTypesEnd);
if (innerTypesIndex >= 0)
{
newType = newType.Insert(innerTypesIndex, string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)));
}
}
return newType;
}
/// <summary>
/// Converts a string representation of a type to a redirected type.
/// </summary>
/// <param name="type">Type to convert.</param>
/// <returns>String representing the type that represents the redirected type.</returns>
private string ConvertTypeToNewType(string type)
{
const string InnerTypesEnd = ",";
string newType = type;
string[] innerTypes = GetInnerTypes(newType);
if (innerTypes.Length > 0)
{
newType = newType.Replace(string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)), string.Empty);
for (int i = 0; i < innerTypes.Length; i++)
{
innerTypes[i] = ConvertTypeToNewType(innerTypes[i]);
}
}
if (_redirectAttributes.ContainsKey(newType))
{
newType = _redirectAttributes[newType].TypeToLoad;
}
if (innerTypes.Length > 0)
{
int innerTypesIndex = newType.IndexOf(InnerTypesEnd);
if (innerTypesIndex >= 0)
{
newType = newType.Insert(innerTypesIndex, string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)));
}
}
return newType;
}
/// <summary>
/// Returns the inner type of a type, for example, a generic array type.
/// </summary>
/// <param name="type">Full type which might contain an inner type.</param>
/// <returns>Array of inner types.</returns>
private static string[] GetInnerTypes(string type)
{
const char InnerTypeCountStart = '`';
char[] InnerTypeCountEnd = new[] { '[', '+' };
const char InternalTypeStart = '+';
const char InternalTypeEnd = '[';
const string AllTypesStart = "[[";
const char SingleTypeStart = '[';
const char SingleTypeEnd = ']';
List<string> innerTypes = new List<string>();
try
{
int countIndex = type.IndexOf(InnerTypeCountStart);
if (countIndex == -1)
{
return innerTypes.ToArray();
}
// This is a generic, but does the type definition also contain the inner types?
if (!type.Contains(AllTypesStart))
{
return innerTypes.ToArray();
}
// Get the number of inner types
int innerTypeCountEnd = -1;
foreach (char t in InnerTypeCountEnd)
{
int index = type.IndexOf(t);
if ((index != -1) && ((innerTypeCountEnd == -1) || (index < innerTypeCountEnd)))
{
// This value is more likely to be the one
innerTypeCountEnd = index;
}
}
int innerTypeCount = int.Parse(type.Substring(countIndex + 1, innerTypeCountEnd - countIndex - 1));
// Remove all info until the first inner type
if (!type.Contains(InternalTypeStart.ToString()))
{
// Just remove the info
type = type.Substring(innerTypeCountEnd + 1);
}
else
{
// Remove the index, but not the numbers
int internalTypeEnd = type.IndexOf(InternalTypeEnd);
type = type.Substring(internalTypeEnd + 1);
}
// Get all the inner types
for (int i = 0; i < innerTypeCount; i++)
{
// Get the start & end of this inner type
int innerTypeStart = type.IndexOf(SingleTypeStart);
int innerTypeEnd = innerTypeStart + 1;
int openings = 1;
// Loop until we find the end
while (openings > 0)
{
if (type[innerTypeEnd] == SingleTypeStart)
{
openings++;
}
else if (type[innerTypeEnd] == SingleTypeEnd)
{
openings--;
}
// Increase current pos if we still have openings left
if (openings > 0)
{
innerTypeEnd++;
}
}
innerTypes.Add(type.Substring(innerTypeStart + 1, innerTypeEnd - innerTypeStart - 1));
type = type.Substring(innerTypeEnd + 1);
}
}
catch (Exception ex)
{
Log.Warn(ex, TraceMessages.FailedToRetrieveInnerTypes);
}
return innerTypes.ToArray();
}
#endregion
}
}