//----------------------------------------------------------------------- // // 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 safeToString = obj => obj != null ? toString((T)obj) : null; Func safeToOriginalString = obj => obj != null ? toOriginalString((T)obj) : null; Func safeToT = str => str != null ? toValue(str) : default(T); converters.Add(typeof(T), new ValueMapping(safeToString, safeToOriginalString, safeToT)); } /// /// Creates a that resorts to and /// for the conversion. /// /// The type to create the mapping for. /// The value mapping. private static ValueMapping CreateFallbackMapping(Type type) { Requires.NotNull(type, "type"); return new ValueMapping( obj => obj != null ? obj.ToString() : null, null, str => str != null ? Convert.ChangeType(str, type, CultureInfo.InvariantCulture) : null); } /// /// Creates the default encoder for a given type. /// /// The type to create a for. /// A struct. private static ValueMapping GetDefaultEncoder(Type type) { Requires.NotNull(type, "type"); var converterAttributes = (DefaultEncoderAttribute[])type.GetCustomAttributes(typeof(DefaultEncoderAttribute), false); ErrorUtilities.VerifyInternal(converterAttributes.Length < 2, "Too many attributes applied."); if (converterAttributes.Length == 1) { return new ValueMapping(converterAttributes[0].Encoder); } return CreateFallbackMapping(type); } /// /// 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; } } /// /// Checks whether a type is a nullable value type (i.e. int?) /// /// The type in question. /// True if this is a nullable value type. private static bool IsNonNullableValueType(Type type) { if (!type.IsValueType) { return false; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { return false; } return true; } /// /// Retrieves a previously instantiated encoder of a given type, or creates a new one and stores it for later retrieval as well. /// /// The message part encoder type. /// An instance of the desired encoder. private static IMessagePartEncoder GetEncoder(Type messagePartEncoder) { Requires.NotNull(messagePartEncoder, "messagePartEncoder"); Contract.Ensures(Contract.Result() != null); IMessagePartEncoder encoder; lock (encoders) { if (!encoders.TryGetValue(messagePartEncoder, out encoder)) { try { encoder = encoders[messagePartEncoder] = (IMessagePartEncoder)Activator.CreateInstance(messagePartEncoder); } catch (MissingMethodException ex) { throw ErrorUtilities.Wrap(ex, MessagingStrings.EncoderInstantiationFailed, messagePartEncoder.FullName); } } } return encoder; } /// /// Gets the value of the message part, without converting it to/from a string. /// /// The message instance to read from. /// The value of the member. private object GetValueAsObject(IMessage message) { if (this.property != null) { return this.property.GetValue(message, null); } else { return this.field.GetValue(message); } } /// /// Sets the value of a message part directly with a given value. /// /// The message instance to read from. /// The value to set on the this part. private void SetValueAsObject(IMessage message, object value) { if (this.property != null) { this.property.SetValue(message, value, null); } else { this.field.SetValue(message, value); } } /// /// Converts a string representation of the member's value to the appropriate type. /// /// The string representation of the member's value. /// /// An instance of the appropriate type for setting the member. /// private object ToValue(string value) { return this.converter.StringToValue(value); } /// /// Converts the member's value to its string representation. /// /// The value of the member. /// A value indicating whether a string matching the originally decoded string should be returned (as opposed to a normalized string). /// /// The string representation of the member's value. /// private string ToString(object value, bool originalString) { return originalString ? this.converter.ValueToOriginalString(value) : this.converter.ValueToString(value); } /// /// Validates that the message part and its attribute have agreeable settings. /// /// /// Thrown when a non-nullable value type is set as optional. /// private void ValidateSettings() { if (!this.IsRequired && IsNonNullableValueType(this.memberDeclaredType)) { MemberInfo member = (MemberInfo)this.field ?? this.property; throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, "Invalid combination: {0} on message type {1} is a non-nullable value type but is marked as optional.", member.Name, member.DeclaringType)); } } } }