//-----------------------------------------------------------------------
//
// 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;
using Validation;
///
/// 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.
///
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 {
return this.message;
}
}
///
/// Gets the description of the type of message this dictionary provides access to.
///
public MessageDescription Description {
get {
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() {
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
}
}