//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. 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 safeFromByteArray = bytes => {
Contract.Assume(bytes != null);
return Convert.ToBase64String(bytes);
};
Func safeToByteArray = str => {
Contract.Assume(str != null);
return Convert.FromBase64String(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(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) {
Requires.NotNull(member, "member");
Requires.True(member is FieldInfo || member is PropertyInfo, "member");
Requires.NotNull(attribute, "attribute");
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 = GetDefaultEncoder(underlyingType);
}
} else {
this.converter = GetDefaultEncoder(this.memberDeclaredType);
}
}
} 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 {
Requires.ValidState(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) {
Requires.NotNull(message, "message");
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);
}
}
///
/// 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) {
Requires.NotNull(toString, "toString");
Requires.NotNull(toValue, "toValue");
if (toOriginalString == null) {
toOriginalString = toString;
}
Func