|
// <copyright file="JsonCollectionDeserializer.cs" company="Kevin Locke"> |
|
// Copyright 2018 Kevin Locke |
|
// |
|
// Permission to use, copy, modify, and/or distribute this software for any |
|
// purpose with or without fee is hereby granted, provided that the above |
|
// copyright notice and this permission notice appear in all copies. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY |
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION |
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN |
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
// </copyright> |
|
|
|
namespace KevinLocke.Json |
|
{ |
|
using System; |
|
using System.Collections; |
|
using System.Collections.Specialized; |
|
using System.Diagnostics; |
|
using System.Globalization; |
|
using System.IO; |
|
using System.Runtime.Serialization.Json; |
|
using System.Text; |
|
using System.Xml; |
|
|
|
public class JsonCollectionDeserializer |
|
{ |
|
/// <summary> |
|
/// Initializes a new instance of the |
|
/// <see cref="JsonCollectionDeserializer"/> class using the default |
|
/// collection type factories. |
|
/// </summary> |
|
public JsonCollectionDeserializer() |
|
: this(null, null) |
|
{ |
|
} |
|
|
|
/// <summary> |
|
/// Initializes a new instance of the |
|
/// <see cref="JsonCollectionDeserializer"/> class using the given |
|
/// collection type factories. |
|
/// </summary> |
|
/// <param name="arrayFactory">Factory function for <see cref="IList"/> |
|
/// instances which will hold JSON array values.</param> |
|
/// <param name="objectFactory">Factory function for <see cref="IDictionary"/> |
|
/// instances which will hold JSON object values.</param> |
|
public JsonCollectionDeserializer(Func<IList> arrayFactory, Func<IDictionary> objectFactory) |
|
{ |
|
this.ArrayFactory = arrayFactory ?? (() => new ArrayList()); |
|
this.ObjectFactory = objectFactory ?? (() => new OrderedDictionary()); |
|
} |
|
|
|
/// <summary> |
|
/// Gets a factory function for <see cref="IList"/> instances which |
|
/// will hold JSON array values. |
|
/// </summary> |
|
protected Func<IList> ArrayFactory { get; } |
|
|
|
/// <summary> |
|
/// Gets a factory function for <see cref="IDictionary"/> instances |
|
/// which will hold JSON object values. |
|
/// </summary> |
|
protected Func<IDictionary> ObjectFactory { get; } |
|
|
|
/// <summary> |
|
/// Deserializes a JSON value from a <see cref="Stream"/> with a |
|
/// given quota. |
|
/// </summary> |
|
/// <param name="stream">Stream from which to deserialize a JSON |
|
/// value.</param> |
|
/// <param name="quotas">Quotas to apply during deserialization to |
|
/// prevent attacks from malicious JSON.</param> |
|
/// <returns>The deserialized JSON value.</returns> |
|
/// <seealso cref="JsonReaderWriterFactory.CreateJsonReader(Stream, XmlDictionaryReaderQuotas)"/> |
|
public virtual object Deserialize(Stream stream, XmlDictionaryReaderQuotas quotas) |
|
{ |
|
using (XmlReader xmlReader = |
|
JsonReaderWriterFactory.CreateJsonReader(stream, quotas)) |
|
{ |
|
return this.Deserialize(xmlReader); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Deserializes a JSON value from given bytes with a given quota. |
|
/// </summary> |
|
/// <param name="bytes">Bytes from which to deserialize a JSON |
|
/// value.</param> |
|
/// <param name="quotas">Quotas to apply during deserialization to |
|
/// prevent attacks from malicious JSON.</param> |
|
/// <returns>The deserialized JSON value.</returns> |
|
/// <seealso cref="JsonReaderWriterFactory.CreateJsonReader(byte[], XmlDictionaryReaderQuotas)"/> |
|
public virtual object Deserialize(byte[] bytes, XmlDictionaryReaderQuotas quotas) |
|
{ |
|
using (XmlReader xmlReader = |
|
JsonReaderWriterFactory.CreateJsonReader(bytes, quotas)) |
|
{ |
|
return this.Deserialize(xmlReader); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Deserializes a JSON value from an <see cref="XmlReader"/> in the |
|
/// format returned by |
|
/// <see cref="JsonReaderWriterFactory.CreateJsonReader(Stream,XmlDictionaryReaderQuotas)"/>. |
|
/// |
|
/// The format is (mostly) documented at: |
|
/// https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/mapping-between-json-and-xml |
|
/// </summary> |
|
/// <param name="reader">XmlReader from which to read a JSON value.</param> |
|
/// <returns>A JSON value read from <paramref name="reader"/>.</returns> |
|
/// <exception cref="ArgumentException">If <paramref name="reader"/> |
|
/// does not contain a JSON value.</exception> |
|
public virtual object Deserialize(XmlReader reader) |
|
{ |
|
if (!this.ReadNameAndValue(reader, out string name, out object value)) |
|
{ |
|
throw new ArgumentException("No JSON in source.", nameof(reader)); |
|
} |
|
|
|
if (name != "root") |
|
{ |
|
Trace.WriteLine( |
|
$"Expected root element name \"root\", got \"{name}\".", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
|
|
return value; |
|
} |
|
|
|
/// <summary> |
|
/// Reads a JSON name/value pair at the current |
|
/// <see cref="XmlReader.Depth"/>. |
|
/// </summary> |
|
/// <param name="reader">XmlReader from which to read.</param> |
|
/// <param name="name">Name of the JSON value.</param> |
|
/// <param name="value">JSON value.</param> |
|
/// <returns><c>true</c> if a JSON name/value pair was read at the |
|
/// current Depth (i.e. without an unpaired EndElement), <c>false</c> |
|
/// otherwise. <paramref name="reader"/> will have |
|
/// <see cref="XmlReader.NodeType"/> <see cref="XmlNodeType.EndElement"/> |
|
/// (or <see cref="XmlReader.EOF"/>).</returns> |
|
protected virtual bool ReadNameAndValue(XmlReader reader, out string name, out object value) |
|
{ |
|
while (this.ReadNameAndType(reader, out name, out string jsonType)) |
|
{ |
|
switch (jsonType) |
|
{ |
|
case "object": |
|
IDictionary objectValue = this.ObjectFactory(); |
|
while (this.ReadNameAndValue(reader, out string key, out object item)) |
|
{ |
|
objectValue.Add(key, item); |
|
} |
|
|
|
value = objectValue; |
|
return true; |
|
|
|
case "array": |
|
IList arrayValue = this.ArrayFactory(); |
|
while (this.ReadNameAndValue(reader, out string key, out object item)) |
|
{ |
|
if (key != "item") |
|
{ |
|
Trace.WriteLine( |
|
$"Expected array child name \"item\", got \"{key}\".", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
|
|
arrayValue.Add(item); |
|
} |
|
|
|
value = arrayValue; |
|
return true; |
|
|
|
case "number": |
|
string numberStr = this.ReadContentForType(reader, jsonType); |
|
if (!string.IsNullOrEmpty(numberStr)) |
|
{ |
|
try |
|
{ |
|
value = double.Parse( |
|
numberStr, |
|
NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, |
|
NumberFormatInfo.InvariantInfo); |
|
return true; |
|
} |
|
catch (FormatException ex) |
|
{ |
|
Trace.WriteLine( |
|
$"Error parsing type=\"{jsonType}\": {ex.Message}", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
catch (OverflowException ex) |
|
{ |
|
Trace.WriteLine( |
|
$"Error parsing type=\"{jsonType}\": {ex.Message}", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
} |
|
else |
|
{ |
|
Trace.WriteLine( |
|
$"Missing value for type=\"{jsonType}\". Ignored.", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
|
|
break; |
|
|
|
case "boolean": |
|
string boolStr = this.ReadContentForType(reader, jsonType); |
|
if (boolStr == "true") |
|
{ |
|
value = true; |
|
return true; |
|
} |
|
else if (boolStr == "false") |
|
{ |
|
value = false; |
|
return true; |
|
} |
|
else |
|
{ |
|
Trace.WriteLine( |
|
$"Unrecognized value for type=\"{jsonType}\": \"{boolStr}\"", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
|
|
break; |
|
|
|
case "string": |
|
value = this.ReadContentForType(reader, jsonType); |
|
return true; |
|
|
|
case "null": |
|
reader.Read(); |
|
while (reader.NodeType != XmlNodeType.EndElement && !reader.EOF) |
|
{ |
|
if (reader.NodeType == XmlNodeType.Element) |
|
{ |
|
Trace.WriteLine( |
|
$"Ignoring {reader.NodeType} {reader.Name} and descendants inside type=\"{jsonType}\".", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
else if (reader.NodeType != XmlNodeType.Comment) |
|
{ |
|
Trace.WriteLine( |
|
$"Ignoring {reader.NodeType} {reader.Name} {reader.Value} inside type=\"{jsonType}\".", |
|
nameof(JsonCollectionDeserializer)); |
|
} |
|
|
|
reader.Skip(); |
|
} |
|
|
|
value = null; |
|
return true; |
|
|
|
default: |
|
Trace.WriteLine( |
|
$"Unrecognized type {jsonType}.", |
|
nameof(JsonCollectionDeserializer)); |
|
break; |
|
} |
|
} |
|
|
|
value = null; |
|
return false; |
|
} |
|
|
|
/// <summary> |
|
/// Reads to the first JSON <see cref="XmlNodeType.Element"/> at the |
|
/// current <see cref="XmlReader.Depth"/>. |
|
/// </summary> |
|
/// <param name="reader">XmlReader from which to read.</param> |
|
/// <param name="name">Name of the JSON value.</param> |
|
/// <param name="jsonType">JSON value type.</param> |
|
/// <returns><c>true</c> if a JSON Element was read at the current |
|
/// Depth (i.e. without an unpaired EndElement), <c>false</c> |
|
/// otherwise. <paramref name="reader"/> will have |
|
/// <see cref="XmlReader.NodeType"/> <see cref="XmlNodeType.Element"/> |
|
/// when <c>true</c> and <see cref="XmlNodeType.EndElement"/> |
|
/// (or <see cref="XmlReader.EOF"/>) when <c>false</c>.</returns> |
|
private bool ReadNameAndType(XmlReader reader, out string name, out string jsonType) |
|
{ |
|
while (reader.Read()) |
|
{ |
|
switch (reader.NodeType) |
|
{ |
|
case XmlNodeType.Element: |
|
string tempName = null; |
|
string namespaceUri = reader.NamespaceURI; |
|
if (string.IsNullOrEmpty(namespaceUri)) |
|
{ |
|
tempName = reader.LocalName; |
|
} |
|
|
|
// Undocumented handling of invalid element names |
|
else if (namespaceUri == "item" |
|
&& reader.LocalName == "item") |
|
{ |
|
tempName = reader.GetAttribute("item"); |
|
} |
|
|
|
if (tempName != null) |
|
{ |
|
name = tempName; |
|
jsonType = reader.GetAttribute("type") ?? "string"; |
|
return true; |
|
} |
|
|
|
Trace.WriteLine( |
|
$"Ignoring {reader.NodeType} {reader.Name} and descendants.", |
|
nameof(JsonCollectionDeserializer)); |
|
int depth = reader.Depth; |
|
while (reader.Read() && reader.Depth > depth) |
|
{ |
|
// Skip descendant nodes. |
|
} |
|
|
|
break; |
|
|
|
case XmlNodeType.EndElement: |
|
goto DoneReading; |
|
|
|
case XmlNodeType.Comment: |
|
// Ignore silently |
|
break; |
|
|
|
default: |
|
Trace.WriteLine( |
|
$"Ignoring {reader.NodeType} {reader.Name} {reader.Value}", |
|
nameof(JsonCollectionDeserializer)); |
|
break; |
|
} |
|
} |
|
|
|
DoneReading: |
|
name = null; |
|
jsonType = null; |
|
return false; |
|
} |
|
|
|
/// <summary> |
|
/// Reads the text content of the current element, excluding descendants. |
|
/// |
|
/// Like <see cref="XmlReader.ReadContentAsString"/> except it expects |
|
/// to be called on an <see cref="XmlNodeType.Element"/> and doesn't |
|
/// stop reading at the first element. |
|
/// </summary> |
|
/// <param name="reader">XmlReader from which to read content. |
|
/// Must have <see cref="XmlReader.NodeType"/> |
|
/// <see cref="XmlNodeType.Element"/>.</param> |
|
/// <param name="jsonType">Element type (for logging only).</param> |
|
/// <returns>String content of child nodes of the current</returns> |
|
private string ReadContentForType(XmlReader reader, string jsonType) |
|
{ |
|
string firstContent = string.Empty; |
|
StringBuilder stringBuilder = null; |
|
reader.Read(); |
|
while (!reader.EOF) |
|
{ |
|
switch (reader.NodeType) |
|
{ |
|
case XmlNodeType.Element: |
|
Trace.WriteLine( |
|
$"Ignoring {reader.NodeType} {reader.Name} and descendants inside Element with type=\"{jsonType}\".", |
|
nameof(JsonCollectionDeserializer)); |
|
reader.Skip(); |
|
break; |
|
|
|
case XmlNodeType.EndElement: |
|
goto DoneReading; |
|
|
|
default: |
|
if (firstContent.Length == 0) |
|
{ |
|
firstContent = reader.ReadContentAsString(); |
|
} |
|
else |
|
{ |
|
if (stringBuilder == null) |
|
{ |
|
stringBuilder = new StringBuilder(firstContent); |
|
} |
|
|
|
stringBuilder.Append(reader.ReadContentAsString()); |
|
} |
|
|
|
break; |
|
} |
|
} |
|
|
|
DoneReading: |
|
return stringBuilder?.ToString() ?? firstContent; |
|
} |
|
} |
|
} |