Merge pull request #487 from mayerwin/patch-1
[mono.git] / mcs / class / System.Json.Microsoft / System.Json / JsonObject.cs
1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
2
3 using System.Collections;
4 using System.Collections.Generic;
5 using System.Diagnostics.CodeAnalysis;
6 using System.Runtime.Serialization;
7 using System.Xml;
8 using WrappedPair = System.Json.NGenWrapper<System.Collections.Generic.KeyValuePair<string, System.Json.JsonValue>>;
9
10 namespace System.Json
11 {
12     /// <summary>
13     /// A JsonObject is an unordered collection of zero or more key/value pairs.
14     /// </summary>
15     /// <remarks>A JsonObject is an unordered collection of zero or more key/value pairs,
16     /// where each key is a String and each value is a <see cref="System.Json.JsonValue"/>, which can be a
17     /// <see cref="System.Json.JsonPrimitive"/>, a <see cref="System.Json.JsonArray"/>, or a JsonObject.</remarks>
18     [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
19         Justification = "Object in the context of JSON already conveys the meaning of dictionary")]
20     [DataContract]
21     public sealed class JsonObject : JsonValue, IDictionary<string, JsonValue>
22     {
23         [DataMember]
24         private Dictionary<string, JsonValue> values = new Dictionary<string, JsonValue>(StringComparer.Ordinal);
25
26         private List<WrappedPair> indexedPairs;
27         private int instanceSaveCount;
28         private object saveLock = new object();
29
30         /// <summary>
31         /// Creates an instance of the <see cref="System.Json.JsonObject"/> class initialized with an
32         /// <see cref="System.Collections.Generic.IEnumerable{T}"/> collection of key/value pairs.
33         /// </summary>
34         /// <param name="items">The <see cref="System.Collections.Generic.IEnumerable{T}"/> collection of
35         /// <see cref="System.Collections.Generic.KeyValuePair{K, V}"/> used to initialize the
36         /// key/value pairs</param>
37         /// <exception cref="System.ArgumentNullException">If items is null.</exception>
38         /// <exception cref="System.ArgumentException">If any of the values in the collection
39         /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
40         /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
41         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
42             Justification = "There's no complexity using this design because nested generic type is atomic type not another collection")]
43         public JsonObject(IEnumerable<KeyValuePair<string, JsonValue>> items)
44         {
45             AddRange(items);
46         }
47
48         /// <summary>
49         /// Creates an instance of the <see cref="System.Json.JsonObject"/> class initialized with a collection of key/value pairs.
50         /// </summary>
51         /// <param name="items">The <see cref="System.Collections.Generic.KeyValuePair{K, V}"/> objects used to initialize the key/value pairs.</param>
52         /// <exception cref="System.ArgumentException">If any of the values in the collection
53         /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
54         /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
55         public JsonObject(params KeyValuePair<string, JsonValue>[] items)
56         {
57             if (items != null)
58             {
59                 AddRange(items);
60             }
61         }
62
63         /// <summary>
64         /// Gets the JSON type of this <see cref="System.Json.JsonObject"/>. The return value
65         /// is always <see cref="F:System.Json.JsonType.Object"/>.
66         /// </summary>
67         public override JsonType JsonType
68         {
69             get { return JsonType.Object; }
70         }
71
72         /// <summary>
73         /// Gets a collection that contains the keys in this <see cref="System.Json.JsonObject"/>.
74         /// </summary>
75         public ICollection<string> Keys
76         {
77             get { return values.Keys; }
78         }
79
80         /// <summary>
81         /// Gets a collection that contains the values in this <see cref="System.Json.JsonObject"/>.
82         /// </summary>
83         public ICollection<JsonValue> Values
84         {
85             get { return values.Values; }
86         }
87
88         /// <summary>
89         /// Returns the number of key/value pairs in this <see cref="System.Json.JsonObject"/>.
90         /// </summary>
91         public override int Count
92         {
93             get { return values.Count; }
94         }
95
96         /// <summary>
97         /// Gets a value indicating whether this JSON CLR object is read-only.
98         /// </summary>
99         bool ICollection<KeyValuePair<string, JsonValue>>.IsReadOnly
100         {
101             get { return ((ICollection<KeyValuePair<string, JsonValue>>)values).IsReadOnly; }
102         }
103
104         /// <summary>
105         /// Gets or sets the value associated with the specified key.
106         /// </summary>
107         /// <param name="key">The key of the value to get or set.</param>
108         /// <returns>The <see cref="System.Json.JsonValue"/> associated to the specified key.</returns>
109         /// <exception cref="System.ArgumentNullException">If key is null.</exception>
110         /// <exception cref="System.ArgumentException">The property is set and the value is a
111         /// <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/>
112         /// property of value <see cref="F:System.Json.JsonType.Default"/>.</exception>
113         public override JsonValue this[string key]
114         {
115             get
116             {
117                 if (key == null)
118                 {
119                     throw new ArgumentNullException("key");
120                 }
121
122                 return values[key];
123             }
124
125             set
126             {
127                 if (value != null && value.JsonType == JsonType.Default)
128                 {
129                     throw new ArgumentNullException("value", Properties.Resources.UseOfDefaultNotAllowed);
130                 }
131
132                 if (key == null)
133                 {
134                     throw new ArgumentNullException("key");
135                 }
136
137                 bool replacement = values.ContainsKey(key);
138                 JsonValue oldValue = null;
139                 if (replacement)
140                 {
141                     oldValue = values[key];
142                     RaiseItemChanging(value, JsonValueChange.Replace, key);
143                 }
144                 else
145                 {
146                     RaiseItemChanging(value, JsonValueChange.Add, key);
147                 }
148
149                 values[key] = value;
150                 if (replacement)
151                 {
152                     RaiseItemChanged(oldValue, JsonValueChange.Replace, key);
153                 }
154                 else
155                 {
156                     RaiseItemChanged(value, JsonValueChange.Add, key);
157                 }
158             }
159         }
160
161         /// <summary>
162         /// Safe string indexer for the <see cref="System.Json.JsonValue"/> type. 
163         /// </summary>
164         /// <param name="key">The key of the element to get.</param>
165         /// <returns>If this instance contains the given key and the value corresponding to
166         /// the key is not null, then it will return that value. Otherwise it will return a
167         /// <see cref="System.Json.JsonValue"/> instance with <see cref="System.Json.JsonValue.JsonType"/>
168         /// equals to <see cref="F:System.Json.JsonType.Default"/>.</returns>
169         public override JsonValue ValueOrDefault(string key)
170         {
171             if (key != null && ContainsKey(key) && this[key] != null)
172             {
173                 return this[key];
174             }
175
176             return base.ValueOrDefault(key);
177         }
178
179         /// <summary>
180         /// Adds a specified collection of key/value pairs to this instance.
181         /// </summary>
182         /// <param name="items">The collection of key/value pairs to add.</param>
183         /// <exception cref="System.ArgumentNullException">If items is null.</exception>
184         /// <exception cref="System.ArgumentException">If the value of any of the items in the collection
185         /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
186         /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
187         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
188             Justification = "There's no complexity using this design because nested generic type is atomic type not another collection")]
189         public void AddRange(IEnumerable<KeyValuePair<string, JsonValue>> items)
190         {
191             if (items == null)
192             {
193                 throw new ArgumentNullException("items");
194             }
195
196             if (ChangingListenersCount > 0)
197             {
198                 foreach (KeyValuePair<string, JsonValue> item in items)
199                 {
200                     RaiseItemChanging(item.Value, JsonValueChange.Add, item.Key);
201                 }
202             }
203
204             foreach (KeyValuePair<string, JsonValue> item in items)
205             {
206                 if (item.Value != null && item.Value.JsonType == JsonType.Default)
207                 {
208                     throw new ArgumentNullException("items", Properties.Resources.UseOfDefaultNotAllowed);
209                 }
210
211                 values.Add(item.Key, item.Value);
212                 RaiseItemChanged(item.Value, JsonValueChange.Add, item.Key);
213             }
214         }
215
216         /// <summary>
217         /// Adds the elements from an array of type <see cref="System.Json.JsonValue"/> to this instance.
218         /// </summary>
219         /// <param name="items">The array of key/value paris to be added to this instance.</param>
220         /// <exception cref="System.ArgumentException">If the value of any of the items in the array
221         /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
222         /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
223         public void AddRange(params KeyValuePair<string, JsonValue>[] items)
224         {
225             AddRange(items as IEnumerable<KeyValuePair<string, JsonValue>>);
226         }
227
228         /// <summary>
229         /// 
230         /// </summary>
231         /// <returns></returns>
232         IEnumerator IEnumerable.GetEnumerator()
233         {
234             return ((IEnumerable)values).GetEnumerator();
235         }
236
237         /// <summary>
238         /// Adds a key/value pair to this <see cref="System.Json.JsonObject"/> instance.
239         /// </summary>
240         /// <param name="key">The key for the element added.</param>
241         /// <param name="value">The <see cref="System.Json.JsonValue"/> for the element added.</param>
242         /// <exception cref="System.ArgumentException">If the value is a <see cref="System.Json.JsonValue"/>
243         /// with <see cref="System.Json.JsonValue.JsonType"/> property of
244         /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
245         public void Add(string key, JsonValue value)
246         {
247             if (value != null && value.JsonType == JsonType.Default)
248             {
249                 throw new ArgumentNullException("value", Properties.Resources.UseOfDefaultNotAllowed);
250             }
251
252             RaiseItemChanging(value, JsonValueChange.Add, key);
253             values.Add(key, value);
254             RaiseItemChanged(value, JsonValueChange.Add, key);
255         }
256
257         /// <summary>
258         /// Adds a key/value pair to this <see cref="System.Json.JsonObject"/> instance.
259         /// </summary>
260         /// <param name="item">The key/value pair to be added.</param>
261         /// <exception cref="System.ArgumentException">If the value of the pair is a
262         /// <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/>
263         /// property of value <see cref="F:System.Json.JsonType.Default"/>.</exception>
264         public void Add(KeyValuePair<string, JsonValue> item)
265         {
266             Add(item.Key, item.Value);
267         }
268
269         /// <summary>
270         /// Checks whether a key/value pair with a specified key exists in this <see cref="System.Json.JsonObject"/> instance.
271         /// </summary>
272         /// <param name="key">The key to check for.</param>
273         /// <returns>true if this instance contains the key; otherwise, false.</returns>
274         public override bool ContainsKey(string key)
275         {
276             return values.ContainsKey(key);
277         }
278
279         /// <summary>
280         /// Removes the key/value pair with a specified key from this <see cref="System.Json.JsonObject"/> instance.
281         /// </summary>
282         /// <param name="key">The key of the item to remove.</param>
283         /// <returns>true if the element is successfully found and removed; otherwise, false.
284         /// This method returns false if key is not found in this <see cref="System.Json.JsonObject"/> instance.</returns>
285         public bool Remove(string key)
286         {
287             JsonValue original = null;
288             bool containsKey = false;
289             if (ChangingListenersCount > 0 || ChangedListenersCount > 0)
290             {
291                 containsKey = TryGetValue(key, out original);
292             }
293
294             if (containsKey && ChangingListenersCount > 0)
295             {
296                 RaiseItemChanging(original, JsonValueChange.Remove, key);
297             }
298
299             bool result = values.Remove(key);
300
301             if (containsKey && ChangedListenersCount > 0)
302             {
303                 RaiseItemChanged(original, JsonValueChange.Remove, key);
304             }
305
306             return result;
307         }
308
309         /// <summary>
310         /// Attempts to get the value that corresponds to the specified key.
311         /// </summary>
312         /// <param name="key">The key of the value to retrieve.</param>
313         /// <param name="value">The primitive or structured <see cref="System.Json.JsonValue"/> object that has the key
314         /// specified. If this object does not contain a key/value pair with the given key,
315         /// this parameter is set to null.</param>
316         /// <returns>true if the instance of the <see cref="System.Json.JsonObject"/> contains an element with the
317         /// specified key; otherwise, false.</returns>
318         public bool TryGetValue(string key, out JsonValue value)
319         {
320             return values.TryGetValue(key, out value);
321         }
322
323         /// <summary>
324         /// Removes all key/value pairs from this <see cref="System.Json.JsonObject"/> instance.
325         /// </summary>
326         public void Clear()
327         {
328             RaiseItemChanging(null, JsonValueChange.Clear, null);
329             values.Clear();
330             RaiseItemChanged(null, JsonValueChange.Clear, null);
331         }
332
333         bool ICollection<KeyValuePair<string, JsonValue>>.Contains(KeyValuePair<string, JsonValue> item)
334         {
335             return ((ICollection<KeyValuePair<string, JsonValue>>)values).Contains(item);
336         }
337
338         /// <summary>
339         /// Copies the contents of this <see cref="System.Json.JsonObject"/> instance into a specified
340         /// key/value destination array beginning at a specified index.
341         /// </summary>
342         /// <param name="array">The destination array of type <see cref="System.Collections.Generic.KeyValuePair{K, V}"/>
343         /// to which the elements of this <see cref="System.Json.JsonObject"/> are copied.</param>
344         /// <param name="arrayIndex">The zero-based index at which to begin the insertion of the
345         /// contents from this <see cref="System.Json.JsonObject"/> instance.</param>
346         public void CopyTo(KeyValuePair<string, JsonValue>[] array, int arrayIndex)
347         {
348             ((ICollection<KeyValuePair<string, JsonValue>>)values).CopyTo(array, arrayIndex);
349         }
350
351         bool ICollection<KeyValuePair<string, JsonValue>>.Remove(KeyValuePair<string, JsonValue> item)
352         {
353             if (ChangingListenersCount > 0)
354             {
355                 if (ContainsKey(item.Key) && EqualityComparer<JsonValue>.Default.Equals(item.Value, values[item.Key]))
356                 {
357                     RaiseItemChanging(item.Value, JsonValueChange.Remove, item.Key);
358                 }
359             }
360
361             bool result = ((ICollection<KeyValuePair<string, JsonValue>>)values).Remove(item);
362             if (result)
363             {
364                 RaiseItemChanged(item.Value, JsonValueChange.Remove, item.Key);
365             }
366
367             return result;
368         }
369
370         /// <summary>
371         /// Returns an enumerator over the key/value pairs contained in this <see cref="System.Json.JsonObject"/> instance.
372         /// </summary>
373         /// <returns>An <see cref="System.Collections.Generic.IEnumerator{T}"/> which iterates
374         /// through the members of this instance.</returns>
375         protected override IEnumerator<KeyValuePair<string, JsonValue>> GetKeyValuePairEnumerator()
376         {
377             return values.GetEnumerator();
378         }
379
380         /// <summary>
381         /// Callback method called when a Save operation is starting for this instance.
382         /// </summary>
383         protected override void OnSaveStarted()
384         {
385             lock (saveLock)
386             {
387                 instanceSaveCount++;
388                 if (indexedPairs == null)
389                 {
390                     indexedPairs = new List<WrappedPair>();
391
392                     foreach (KeyValuePair<string, JsonValue> item in values)
393                     {
394                         indexedPairs.Add(new WrappedPair(item));
395                     }
396                 }
397             }
398         }
399
400         /// <summary>
401         /// Callback method called when a Save operation is finished for this instance.
402         /// </summary>
403         protected override void OnSaveEnded()
404         {
405             lock (saveLock)
406             {
407                 instanceSaveCount--;
408                 if (instanceSaveCount == 0)
409                 {
410                     indexedPairs = null;
411                 }
412             }
413         }
414
415         /// <summary>
416         /// Callback method called to let an instance write the proper JXML attribute when saving this
417         /// instance.
418         /// </summary>
419         /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
420         internal override void WriteAttributeString(XmlDictionaryWriter jsonWriter)
421         {
422             jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.ObjectAttributeValue);
423         }
424
425         /// <summary>
426         /// Callback method called during Save operations to let the instance write the start element
427         /// and return the next element in the collection.
428         /// </summary>
429         /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
430         /// <param name="currentIndex">The index within this collection.</param>
431         /// <returns>The next item in the collection, or null of there are no more items.</returns>
432         internal override JsonValue WriteStartElementAndGetNext(XmlDictionaryWriter jsonWriter, int currentIndex)
433         {
434             KeyValuePair<string, JsonValue> currentPair = indexedPairs[currentIndex];
435             string currentKey = currentPair.Key;
436
437             if (currentKey.Length == 0)
438             {
439                 // special case in JXML world
440                 jsonWriter.WriteStartElement(JXmlToJsonValueConverter.ItemElementName, JXmlToJsonValueConverter.ItemElementName);
441                 jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.ItemElementName, String.Empty);
442             }
443             else
444             {
445                 jsonWriter.WriteStartElement(currentKey);
446             }
447
448             return currentPair.Value;
449         }
450
451         [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Context is required by CLR for this to work.")]
452         [OnDeserialized]
453         private void OnDeserialized(StreamingContext context)
454         {
455             saveLock = new object();
456         }
457
458         private void RaiseItemChanging(JsonValue child, JsonValueChange change, string key)
459         {
460             if (ChangingListenersCount > 0)
461             {
462                 RaiseChangingEvent(this, new JsonValueChangeEventArgs(child, change, key));
463             }
464         }
465
466         private void RaiseItemChanged(JsonValue child, JsonValueChange change, string key)
467         {
468             if (ChangedListenersCount > 0)
469             {
470                 RaiseChangedEvent(this, new JsonValueChangeEventArgs(child, change, key));
471             }
472         }
473     }
474 }