//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.Messaging.Reflection { using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; /// /// Wraps an instance in a dictionary that /// provides access to both well-defined message properties and "extra" /// name/value pairs that have no properties associated with them. /// [ContractVerification(false)] internal class MessageDictionary : IDictionary { /// /// The instance manipulated by this dictionary. /// private readonly IMessage message; /// /// The instance that describes the message type. /// private readonly MessageDescription description; /// /// Whether original string values should be retrieved instead of normalized ones. /// private readonly bool getOriginalValues; /// /// Initializes a new instance of the class. /// /// The message instance whose values will be manipulated by this dictionary. /// The message description. /// A value indicating whether this message dictionary will retrieve original values instead of normalized ones. [Pure] internal MessageDictionary(IMessage message, MessageDescription description, bool getOriginalValues) { Requires.NotNull(message, "message"); Requires.NotNull(description, "description"); this.message = message; this.description = description; this.getOriginalValues = getOriginalValues; } /// /// Gets the message this dictionary provides access to. /// public IMessage Message { get { Contract.Ensures(Contract.Result() != null); return this.message; } } /// /// Gets the description of the type of message this dictionary provides access to. /// public MessageDescription Description { get { Contract.Ensures(Contract.Result() != null); return this.description; } } #region ICollection> Properties /// /// Gets the number of explicitly set values in the message. /// public int Count { get { return this.Keys.Count; } } /// /// Gets a value indicating whether this message is read only. /// bool ICollection>.IsReadOnly { get { return false; } } #endregion #region IDictionary Properties /// /// Gets all the keys that have values associated with them. /// public ICollection Keys { get { List keys = new List(this.message.ExtraData.Count + this.description.Mapping.Count); keys.AddRange(this.DeclaredKeys); keys.AddRange(this.AdditionalKeys); return keys.AsReadOnly(); } } /// /// Gets the set of official message part names that have non-null values associated with them. /// public ICollection DeclaredKeys { get { List keys = new List(this.description.Mapping.Count); foreach (var pair in this.description.Mapping) { // Don't include keys with null values, but default values for structs is ok if (pair.Value.GetValue(this.message, this.getOriginalValues) != null) { keys.Add(pair.Key); } } return keys.AsReadOnly(); } } /// /// Gets the keys that are in the message but not declared as official OAuth properties. /// public ICollection AdditionalKeys { get { return this.message.ExtraData.Keys; } } /// /// Gets all the values. /// public ICollection Values { get { List values = new List(this.message.ExtraData.Count + this.description.Mapping.Count); foreach (MessagePart part in this.description.Mapping.Values) { if (part.GetValue(this.message, this.getOriginalValues) != null) { values.Add(part.GetValue(this.message, this.getOriginalValues)); } } foreach (string value in this.message.ExtraData.Values) { Debug.Assert(value != null, "Null values should never be allowed in the extra data dictionary."); values.Add(value); } return values.AsReadOnly(); } } #endregion /// /// Gets the serializer for the message this dictionary provides access to. /// private MessageSerializer Serializer { get { return MessageSerializer.Get(this.Message.GetType()); } } #region IDictionary Indexers /// /// Gets or sets a value for some named value. /// /// The serialized form of a name for the value to read or write. /// The named value. /// /// If the key matches a declared property or field on the message type, /// that type member is set. Otherwise the key/value is stored in a /// dictionary for extra (weakly typed) strings. /// /// Thrown when setting a value that is not allowed for a given . public string this[string key] { get { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { // Never throw KeyNotFoundException for declared properties. return part.GetValue(this.message, this.getOriginalValues); } else { return this.message.ExtraData[key]; } } set { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { part.SetValue(this.message, value); } else { if (value == null) { this.message.ExtraData.Remove(key); } else { this.message.ExtraData[key] = value; } } } } #endregion #region IDictionary Methods /// /// Adds a named value to the message. /// /// The serialized form of the name whose value is being set. /// The serialized form of the value. /// /// Thrown if already has a set value in this message. /// /// /// Thrown if is null. /// public void Add(string key, string value) { ErrorUtilities.VerifyArgumentNotNull(value, "value"); MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { if (part.IsNondefaultValueSet(this.message)) { throw new ArgumentException(MessagingStrings.KeyAlreadyExists); } part.SetValue(this.message, value); } else { this.message.ExtraData.Add(key, value); } } /// /// Checks whether some named parameter has a value set in the message. /// /// The serialized form of the message part's name. /// True if the parameter by the given name has a set value. False otherwise. public bool ContainsKey(string key) { return this.message.ExtraData.ContainsKey(key) || (this.description.Mapping.ContainsKey(key) && this.description.Mapping[key].GetValue(this.message, this.getOriginalValues) != null); } /// /// Removes a name and value from the message given its name. /// /// The serialized form of the name to remove. /// True if a message part by the given name was found and removed. False otherwise. public bool Remove(string key) { if (this.message.ExtraData.Remove(key)) { return true; } else { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { if (part.GetValue(this.message, this.getOriginalValues) != null) { part.SetValue(this.message, null); return true; } } return false; } } /// /// Gets some named value if the key has a value. /// /// The name (in serialized form) of the value being sought. /// The variable where the value will be set. /// True if the key was found and was set. False otherwise. public bool TryGetValue(string key, out string value) { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { value = part.GetValue(this.message, this.getOriginalValues); return value != null; } return this.message.ExtraData.TryGetValue(key, out value); } #endregion #region ICollection> Methods /// /// Sets a named value in the message. /// /// The name-value pair to add. The name is the serialized form of the key. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")] public void Add(KeyValuePair item) { this.Add(item.Key, item.Value); } /// /// Removes all values in the message. /// public void ClearValues() { foreach (string key in this.Keys) { this.Remove(key); } } /// /// Removes all items from the . /// /// /// The is read-only. /// /// /// This method cannot be implemented because keys are not guaranteed to be removed /// since some are inherent to the type of message that this dictionary provides /// access to. /// public void Clear() { throw new NotSupportedException(); } /// /// Checks whether a named value has been set on the message. /// /// The name/value pair. /// True if the key exists and has the given value. False otherwise. public bool Contains(KeyValuePair item) { MessagePart part; if (this.description.Mapping.TryGetValue(item.Key, out part)) { return string.Equals(part.GetValue(this.message, this.getOriginalValues), item.Value, StringComparison.Ordinal); } else { return this.message.ExtraData.Contains(item); } } /// /// Copies all the serializable data from the message to a key/value array. /// /// The array to copy to. /// The index in the to begin copying to. void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { foreach (var pair in (IDictionary)this) { array[arrayIndex++] = pair; } } /// /// Removes a named value from the message if it exists. /// /// The serialized form of the name and value to remove. /// True if the name/value was found and removed. False otherwise. public bool Remove(KeyValuePair item) { // We use contains because that checks that the value is equal as well. if (((ICollection>)this).Contains(item)) { ((IDictionary)this).Remove(item.Key); return true; } return false; } #endregion #region IEnumerable> Members /// /// Gets an enumerator that generates KeyValuePair<string, string> instances /// for all the key/value pairs that are set in the message. /// /// The enumerator that can generate the name/value pairs. public IEnumerator> GetEnumerator() { foreach (string key in this.Keys) { yield return new KeyValuePair(key, this[key]); } } #endregion #region IEnumerable Members /// /// Gets an enumerator that generates KeyValuePair<string, string> instances /// for all the key/value pairs that are set in the message. /// /// The enumerator that can generate the name/value pairs. IEnumerator System.Collections.IEnumerable.GetEnumerator() { return ((IEnumerable>)this).GetEnumerator(); } #endregion /// /// Saves the data in a message to a standard dictionary. /// /// The generated dictionary. [Pure] public IDictionary Serialize() { Contract.Ensures(Contract.Result>() != null); return this.Serializer.Serialize(this); } /// /// Loads data from a dictionary into the message. /// /// The data to load into the message. public void Deserialize(IDictionary fields) { Requires.NotNull(fields, "fields"); this.Serializer.Deserialize(fields, this); } #if CONTRACTS_FULL /// /// Verifies conditions that should be true for any valid state of this object. /// [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")] [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] [ContractInvariantMethod] private void ObjectInvariant() { Contract.Invariant(this.Message != null); Contract.Invariant(this.Description != null); } #endif } }