AX feature complete, with tests passing.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / Extensions / AttributeExchange / FetchResponse.cs
blob341101228359e2d2644e16cfeaf4b8101b0fb03e
1 //-----------------------------------------------------------------------
2 // <copyright file="FetchResponse.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
4 // </copyright>
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
8 using System;
9 using System.Collections.Generic;
10 using System.Diagnostics;
11 using System.Globalization;
12 using System.Linq;
13 using DotNetOpenAuth.Messaging;
14 using DotNetOpenAuth.OpenId.Messages;
16 /// <summary>
17 /// The Attribute Exchange Fetch message, response leg.
18 /// </summary>
19 public sealed class FetchResponse : ExtensionBase, IMessageWithEvents {
20 /// <summary>
21 /// The factory method that may be used in deserialization of this message.
22 /// </summary>
23 internal static readonly OpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage) => {
24 if (typeUri == Constants.TypeUri && baseMessage is IndirectSignedResponse) {
25 string mode;
26 if (data.TryGetValue("mode", out mode) && mode == Mode) {
27 return new FetchResponse();
31 return null;
34 [MessagePart("mode", IsRequired = true)]
35 private const string Mode = "fetch_response";
37 /// <summary>
38 /// The list of provided attributes. This field will never be null.
39 /// </summary>
40 private readonly List<AttributeValues> attributesProvided = new List<AttributeValues>();
42 /// <summary>
43 /// Initializes a new instance of the <see cref="FetchResponse"/> class.
44 /// </summary>
45 public FetchResponse()
46 : base(new Version(1, 0), Constants.TypeUri, null) {
49 /// <summary>
50 /// Gets a sequence of the attributes whose values are provided by the OpenID Provider.
51 /// </summary>
52 public IEnumerable<AttributeValues> Attributes {
53 get { return this.attributesProvided; }
56 /// <summary>
57 /// Gets a value indicating whether the OpenID Provider intends to
58 /// honor the request for updates.
59 /// </summary>
60 public bool UpdateUrlSupported {
61 get { return this.UpdateUrl != null; }
64 /// <summary>
65 /// Gets or sets the URL the OpenID Provider will post updates to.
66 /// Must be set if the Provider supports and will use this feature.
67 /// </summary>
68 [MessagePart("update_url", IsRequired = false)]
69 public Uri UpdateUrl { get; set; }
71 /// <summary>
72 /// Used by the Provider to add attributes to the response for the relying party.
73 /// </summary>
74 public void AddAttribute(AttributeValues attribute) {
75 ErrorUtilities.VerifyArgumentNotNull(attribute, "attribute");
76 ErrorUtilities.VerifyArgumentNamed(!this.ContainsAttribute(attribute.TypeUri), "attribute", OpenIdStrings.AttributeAlreadyAdded, attribute.TypeUri);
77 this.attributesProvided.Add(attribute);
80 /// <summary>
81 /// Used by the Relying Party to get the value(s) returned by the OpenID Provider
82 /// for a given attribute, or null if that attribute was not provided.
83 /// </summary>
84 public AttributeValues GetAttribute(string attributeTypeUri) {
85 return this.attributesProvided.SingleOrDefault(attribute => string.Equals(attribute.TypeUri, attributeTypeUri, StringComparison.Ordinal));
88 /// <summary>
89 /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
90 /// </summary>
91 /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
92 /// <returns>
93 /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
94 /// </returns>
95 /// <exception cref="T:System.NullReferenceException">
96 /// The <paramref name="obj"/> parameter is null.
97 /// </exception>
98 public override bool Equals(object obj) {
99 FetchResponse other = obj as FetchResponse;
100 if (other == null) {
101 return false;
104 if (this.UpdateUrl != other.UpdateUrl) {
105 return false;
108 if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) {
109 return false;
112 return true;
115 #region IMessageWithEvents Members
117 /// <summary>
118 /// Called when the message is about to be transmitted,
119 /// before it passes through the channel binding elements.
120 /// </summary>
121 void IMessageWithEvents.OnSending() {
122 var extraData = ((IMessage)this).ExtraData;
123 SerializeAttributes(extraData, this.attributesProvided);
126 /// <summary>
127 /// Called when the message has been received,
128 /// after it passes through the channel binding elements.
129 /// </summary>
130 void IMessageWithEvents.OnReceiving() {
131 var extraData = ((IMessage)this).ExtraData;
132 foreach (var att in DeserializeAttributes(extraData)) {
133 this.AddAttribute(att);
137 #endregion
139 internal static void SerializeAttributes(IDictionary<string, string> fields, IEnumerable<AttributeValues> attributes) {
140 ErrorUtilities.VerifyArgumentNotNull(fields, "fields");
141 ErrorUtilities.VerifyArgumentNotNull(attributes, "attributes");
143 AliasManager aliasManager = new AliasManager();
144 foreach (var att in attributes) {
145 string alias = aliasManager.GetAlias(att.TypeUri);
146 fields.Add("type." + alias, att.TypeUri);
147 if (att.Values == null) {
148 continue;
150 if (att.Values.Count != 1) {
151 fields.Add("count." + alias, att.Values.Count.ToString(CultureInfo.InvariantCulture));
152 for (int i = 0; i < att.Values.Count; i++) {
153 fields.Add(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i + 1), att.Values[i]);
155 } else {
156 fields.Add("value." + alias, att.Values[0]);
161 internal static IEnumerable<AttributeValues> DeserializeAttributes(IDictionary<string, string> fields) {
162 AliasManager aliasManager = ParseAliases(fields);
163 foreach (string alias in aliasManager.Aliases) {
164 AttributeValues att = new AttributeValues(aliasManager.ResolveAlias(alias));
165 int count = 1;
166 bool countSent = false;
167 string countString;
168 if (fields.TryGetValue("count." + alias, out countString)) {
169 if (!int.TryParse(countString, out count) || count <= 0) {
170 Logger.ErrorFormat("Failed to parse count.{0} value to a positive integer.", alias);
171 continue;
173 countSent = true;
175 if (countSent) {
176 for (int i = 1; i <= count; i++) {
177 string value;
178 if (fields.TryGetValue(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i), out value)) {
179 att.Values.Add(value);
180 } else {
181 Logger.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri);
182 continue;
185 } else {
186 string value;
187 if (fields.TryGetValue("value." + alias, out value)) {
188 att.Values.Add(value);
189 } else {
190 Logger.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri);
191 continue;
194 yield return att;
198 /// <summary>
199 /// Checks the message state for conformity to the protocol specification
200 /// and throws an exception if the message is invalid.
201 /// </summary>
202 /// <remarks>
203 /// <para>Some messages have required fields, or combinations of fields that must relate to each other
204 /// in specialized ways. After deserializing a message, this method checks the state of the
205 /// message to see if it conforms to the protocol.</para>
206 /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
207 /// outside this scope of this particular message.</para>
208 /// </remarks>
209 /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
210 protected override void EnsureValidMessage() {
211 base.EnsureValidMessage();
213 if (this.UpdateUrl != null && !this.UpdateUrl.IsAbsoluteUri) {
214 this.UpdateUrl = null;
215 Logger.ErrorFormat("The AX fetch response update_url parameter was not absolute ('{0}'). Ignoring value.", this.UpdateUrl);
219 private static AliasManager ParseAliases(IDictionary<string, string> fields) {
220 ErrorUtilities.VerifyArgumentNotNull(fields, "fields");
222 AliasManager aliasManager = new AliasManager();
223 foreach (var pair in fields) {
224 if (!pair.Key.StartsWith("type.", StringComparison.Ordinal)) {
225 continue;
227 string alias = pair.Key.Substring(5);
228 if (alias.IndexOfAny(new[] { '.', ',', ':' }) >= 0) {
229 Logger.ErrorFormat("Illegal characters in alias name '{0}'.", alias);
230 continue;
232 aliasManager.SetAlias(alias, pair.Value);
234 return aliasManager;
237 private bool ContainsAttribute(string typeUri) {
238 return this.GetAttribute(typeUri) != null;