//-----------------------------------------------------------------------
//
// Copyright (c) Andrew Arnott. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Messaging.Reflection {
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Net.Security;
using System.Reflection;
using System.Xml;
using DotNetOpenAuth.Configuration;
///
/// Describes an individual member of a message and assists in its serialization.
///
[ContractVerification(true)]
[DebuggerDisplay("MessagePart {Name}")]
internal class MessagePart {
///
/// A map of converters that help serialize custom objects to string values and back again.
///
private static readonly Dictionary converters = new Dictionary();
///
/// A map of instantiated custom encoders used to encode/decode message parts.
///
private static readonly Dictionary encoders = new Dictionary();
///
/// The string-object conversion routines to use for this individual message part.
///
private ValueMapping converter;
///
/// The property that this message part is associated with, if aplicable.
///
private PropertyInfo property;
///
/// The field that this message part is associated with, if aplicable.
///
private FieldInfo field;
///
/// The type of the message part. (Not the type of the message itself).
///
private Type memberDeclaredType;
///
/// The default (uninitialized) value of the member inherent in its type.
///
private object defaultMemberValue;
///
/// Initializes static members of the class.
///
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This simplifies the rest of the code.")]
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "By design.")]
[SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Much more efficient initialization when we can call methods.")]
static MessagePart() {
Func safeUri = str => {
Contract.Assume(str != null);
return new Uri(str);
};
Func safeBool = str => {
Contract.Assume(str != null);
return bool.Parse(str);
};
////Func safeIdentifier = str => {
//// Contract.Assume(str != null);
//// ErrorUtilities.VerifyFormat(str.Length > 0, MessagingStrings.NonEmptyStringExpected);
//// return Identifier.Parse(str, true);
////};
Func safeFromByteArray = bytes => {
Contract.Assume(bytes != null);
return Convert.ToBase64String(bytes);
};
Func safeToByteArray = str => {
Contract.Assume(str != null);
return Convert.FromBase64String(str);
};
////Func safeRealm = str => {
//// Contract.Assume(str != null);
//// return new Realm(str);
////};
Map(uri => uri.AbsoluteUri, uri => uri.OriginalString, safeUri);
Map(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), null, str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc));
Map(ts => ts.ToString(), null, str => TimeSpan.Parse(str));
Map(safeFromByteArray, null, safeToByteArray);
////Map(realm => realm.ToString(), realm => realm.OriginalString, safeRealm);
////Map(id => id.SerializedString, id => id.OriginalString, safeIdentifier);
Map(value => value.ToString().ToLowerInvariant(), null, safeBool);
Map(c => c.Name, null, str => new CultureInfo(str));
Map(cs => string.Join(",", cs.Select(c => c.Name).ToArray()), null, str => str.Split(',').Select(s => new CultureInfo(s)).ToArray());
Map(t => t.FullName, null, str => Type.GetType(str));
}
///
/// Initializes a new instance of the class.
///
///
/// A property or field of an implementing type
/// that has a attached to it.
///
///
/// The attribute discovered on that describes the
/// serialization requirements of the message part.
///
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Unavoidable"), SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code contracts requires it.")]
internal MessagePart(MemberInfo member, MessagePartAttribute attribute) {
Contract.Requires(member != null);
Contract.Requires(member is FieldInfo || member is PropertyInfo);
Contract.Requires(attribute != null);
this.field = member as FieldInfo;
this.property = member as PropertyInfo;
this.Name = attribute.Name ?? member.Name;
this.RequiredProtection = attribute.RequiredProtection;
this.IsRequired = attribute.IsRequired;
this.AllowEmpty = attribute.AllowEmpty;
this.memberDeclaredType = (this.field != null) ? this.field.FieldType : this.property.PropertyType;
this.defaultMemberValue = DeriveDefaultValue(this.memberDeclaredType);
Contract.Assume(this.memberDeclaredType != null); // CC missing PropertyInfo.PropertyType ensures result != null
if (attribute.Encoder == null) {
if (!converters.TryGetValue(this.memberDeclaredType, out this.converter)) {
if (this.memberDeclaredType.IsGenericType &&
this.memberDeclaredType.GetGenericTypeDefinition() == typeof(Nullable<>)) {
// It's a nullable type. Try again to look up an appropriate converter for the underlying type.
Type underlyingType = Nullable.GetUnderlyingType(this.memberDeclaredType);
ValueMapping underlyingMapping;
if (converters.TryGetValue(underlyingType, out underlyingMapping)) {
this.converter = new ValueMapping(
underlyingMapping.ValueToString,
null,
str => str != null ? underlyingMapping.StringToValue(str) : null);
} else {
this.converter = new ValueMapping(
obj => obj != null ? obj.ToString() : null,
null,
str => str != null ? Convert.ChangeType(str, underlyingType, CultureInfo.InvariantCulture) : null);
}
} else {
this.converter = new ValueMapping(
obj => obj != null ? obj.ToString() : null,
null,
str => str != null ? Convert.ChangeType(str, this.memberDeclaredType, CultureInfo.InvariantCulture) : null);
}
}
} else {
this.converter = new ValueMapping(GetEncoder(attribute.Encoder));
}
// readonly and const fields are considered legal, and "constants" for message transport.
FieldAttributes constAttributes = FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault;
if (this.field != null && (
(this.field.Attributes & FieldAttributes.InitOnly) == FieldAttributes.InitOnly ||
(this.field.Attributes & constAttributes) == constAttributes)) {
this.IsConstantValue = true;
this.IsConstantValueAvailableStatically = this.field.IsStatic;
} else if (this.property != null && !this.property.CanWrite) {
this.IsConstantValue = true;
}
// Validate a sane combination of settings
this.ValidateSettings();
}
///
/// Gets or sets the name to use when serializing or deserializing this parameter in a message.
///
internal string Name { get; set; }
///
/// Gets or sets whether this message part must be signed.
///
internal ProtectionLevel RequiredProtection { get; set; }
///
/// Gets or sets a value indicating whether this message part is required for the
/// containing message to be valid.
///
internal bool IsRequired { get; set; }
///
/// Gets or sets a value indicating whether the string value is allowed to be empty in the serialized message.
///
internal bool AllowEmpty { get; set; }
///
/// Gets or sets a value indicating whether the field or property must remain its default value.
///
internal bool IsConstantValue { get; set; }
///
/// Gets or sets a value indicating whether this part is defined as a constant field and can be read without a message instance.
///
internal bool IsConstantValueAvailableStatically { get; set; }
///
/// Gets the static constant value for this message part without a message instance.
///
internal string StaticConstantValue {
get {
Contract.Requires(this.IsConstantValueAvailableStatically);
return this.ToString(this.field.GetValue(null), false);
}
}
///
/// Gets the type of the declared member.
///
internal Type MemberDeclaredType {
get { return this.memberDeclaredType; }
}
///
/// Sets the member of a given message to some given value.
/// Used in deserialization.
///
/// The message instance containing the member whose value should be set.
/// The string representation of the value to set.
internal void SetValue(IMessage message, string value) {
Contract.Requires(message != null);
try {
if (this.IsConstantValue) {
string constantValue = this.GetValue(message);
var caseSensitivity = DotNetOpenAuthSection.Messaging.Strict ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
if (!string.Equals(constantValue, value, caseSensitivity)) {
throw new ArgumentException(string.Format(
CultureInfo.CurrentCulture,
MessagingStrings.UnexpectedMessagePartValueForConstant,
message.GetType().Name,
this.Name,
constantValue,
value));
}
} else {
this.SetValueAsObject(message, this.ToValue(value));
}
} catch (Exception ex) {
throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartReadFailure, message.GetType(), this.Name, value);
}
}
///
/// Gets the normalized form of a value of a member of a given message.
/// Used in serialization.
///
/// The message instance to read the value from.
/// The string representation of the member's value.
internal string GetValue(IMessage message) {
try {
object value = this.GetValueAsObject(message);
return this.ToString(value, false);
} catch (FormatException ex) {
throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartWriteFailure, message.GetType(), this.Name);
}
}
///
/// Gets the value of a member of a given message.
/// Used in serialization.
///
/// The message instance to read the value from.
/// A value indicating whether the original value should be retrieved (as opposed to a normalized form of it).
/// The string representation of the member's value.
internal string GetValue(IMessage message, bool originalValue) {
try {
object value = this.GetValueAsObject(message);
return this.ToString(value, originalValue);
} catch (FormatException ex) {
throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartWriteFailure, message.GetType(), this.Name);
}
}
///
/// Gets whether the value has been set to something other than its CLR type default value.
///
/// The message instance to check the value on.
/// True if the value is not the CLR default value.
internal bool IsNondefaultValueSet(IMessage message) {
if (this.memberDeclaredType.IsValueType) {
return !this.GetValueAsObject(message).Equals(this.defaultMemberValue);
} else {
return this.defaultMemberValue != this.GetValueAsObject(message);
}
}
///
/// Figures out the CLR default value for a given type.
///
/// The type whose default value is being sought.
/// Either null, or some default value like 0 or 0.0.
private static object DeriveDefaultValue(Type type) {
if (type.IsValueType) {
return Activator.CreateInstance(type);
} else {
return null;
}
}
///
/// Adds a pair of type conversion functions to the static conversion map.
///
/// The custom type to convert to and from strings.
/// The function to convert the custom type to a string.
/// The mapping function that converts some custom value to its original (non-normalized) string. May be null if the same as the function.
/// The function to convert a string to the custom type.
[SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "toString", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "toValue", Justification = "Code contracts")]
private static void Map(Func toString, Func toOriginalString, Func toValue) {
Contract.Requires(toString != null);
Contract.Requires(toValue != null);
if (toOriginalString == null) {
toOriginalString = toString;
}
Func