using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Reflection;
using System.ComponentModel;
using System.Diagnostics;
using System.Collections;
namespace Json
{
/// <summary>
/// JsonExceptions are thrown when an error occurred trying to serialize or deserialize
/// objects derived from <see cref="JsonBase">JsonBase</see>.
/// </summary>
[Serializable]
public class JsonException : Exception
{
/// <summary></summary>
public JsonException() { }
/// <summary></summary>
public JsonException(string message) : base(message) { }
/// <summary></summary>
public JsonException(string message, Exception inner) : base(message, inner) { }
/// <summary></summary>
protected JsonException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context)
: base(info, context) { }
}
/// <summary>
/// Supports a limited version of JSON; see http://json.org for the full spec.
///
/// The current implementation supports objects which contain the following data types:
/// <list type="bullet">
/// <item>string</item>
/// <item>bool</item>
/// <item>byte</item>
/// <item>short</item>
/// <item>int</item>
/// <item>float (untested, might not work)</item>
/// <item>double</item>
/// <item>T : JsonBase</item>
///
/// <item>string[]</item>
/// <item>int[]</item>
/// <item>List{T : JsonBase}</item>
/// <item>List{T : List{S : JsonBase} }</item>
/// </list>
///
/// Arrays of any of the allowed types are supported if deserializion is not required.
/// (I.e., <code>Parse</code> will never be called on it.)
///
/// <note>
/// Classes which derive from JsonBase must provide setters on all non-List<T> properties; if they should be
/// readonly, make the properties <code>protected</code>.
///
/// Scientific notation is not supported. A leading digit is required for numbers with digits to the
/// right of the decimal point; -.1 will not parse. (This can be changed relatively easily if necessary.)
/// </note>
/// </summary>
public abstract class JsonBase
{
public JsonBase()
{ }
/// <summary>
/// Double quotes are not allowed in HTML attributes when attributes are enclosed by
/// double quotes; ASP.NET apparently transforms single-quoted attributes to double
/// quotes before sending rendered output to the client. Therefore, it is most efficient
/// to convert single quotes to an escape sequence and then convert double quotes to
/// single quotes. Otherwise, the size of JSON strings would be considerably enlarged,
/// with " everywhere.
/// </summary>
/// <remarks>
/// NOTE: It is extremely unlikely that the following character sequence will be
/// encountered. If it is, the application will act as if a single quote had been
/// entered, instead of the string below.
///
/// An alternative is to swap single and double quotes and use " -- this could
/// be done relatively simply by writing a for loop that would iterate over a char[];
/// for now that is not necessary and might cause the javascript version to slow down.
/// </remarks>
private const string QuoteReplacement = "~";
public static string EncodeJsonString(string json)
{
Debug.Assert(!json.Contains(QuoteReplacement));
return json.Replace("'", QuoteReplacement).Replace('"', ''');
}
public static string DecodeJsonString(string json)
{
return json.Replace(''', '"').Replace(QuoteReplacement, "'");
}
protected static object Parse(Type jsonType, string json)
{
if (json.Length > 0 && json[0] == '[')
return ParseArray(jsonType, json);
if (!jsonType.IsSubclassOf(typeof(JsonBase)))
throw new ArgumentException("Type '" + jsonType.ToString() + "' is not derived from JsonBase.");
Dictionary<string, object> d = Parse(json);
object o = jsonType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null).Invoke(null);
foreach (PropertyInfo pi in jsonType.GetProperties())
{
if (!d.ContainsKey(pi.Name))
{
object[] oa = pi.GetCustomAttributes(typeof(DefaultValueAttribute), false);
if (oa.Length == 1)
pi.SetValue(o, ((DefaultValueAttribute)oa[0]).Value, null);
continue;
}
try
{
if (pi.PropertyType == typeof(Int32[]))
pi.SetValue(o, Array.ConvertAll<object, int>((object[])d[pi.Name], delegate(object o2) { return int.Parse(o2.ToString()); }), null);
else if (pi.PropertyType == typeof(string[]))
pi.SetValue(o, Array.ConvertAll<object, string>((object[])d[pi.Name], delegate(object o2) { return o2 != null ? o2.ToString() : null; }), null);
else if (pi.PropertyType.IsGenericType)
{
// List<T : JsonBase>
IList list = (IList)pi.GetValue(o, null);
Type t = pi.PropertyType.GetGenericArguments()[0];
foreach (object o2 in (object[])d[pi.Name])
list.Add(Parse(t, (string)o2));
}
else if (pi.PropertyType.IsSubclassOf(typeof(JsonBase)))
pi.SetValue(o, Parse(pi.PropertyType, (string)d[pi.Name]), null);
else
pi.SetValue(o, d[pi.Name], null);
}
catch (Exception ex)
{
throw new JsonException("Cannot assign value to property '" + pi.Name + "' (see InnerException for details).", ex);
}
d.Remove(pi.Name);
}
if (d.Count > 0)
{
string s = "";
foreach (string key in d.Keys)
s += (s.Length > 0 ? ", " : "") + key;
s = "(" + s + ")";
throw new JsonException("The following properties don't exist in type '" + jsonType.ToString() + "': " + s);
}
return o;
}
private static object ParseArray(Type arrayType, string json)
{
Dictionary<string, object> d = Parse("{"Array":" + json + "}");
Type t = arrayType.GetGenericArguments()[0];
IList list = (IList)arrayType.GetConstructor(Type.EmptyTypes).Invoke(null);
foreach (object o in (object[])d["Array"])
list.Add(Parse(t, (string)o));
return list;
}
/// <summary>
/// Returns dictionary representation of the object passed as a JSON string.
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
protected static Dictionary<string, object> Parse(string json)
{
/* Behold the nastiness of escaped slashes and quotes:
*
"(?<key>[^\"]|\.)*"\s*
:\s*
(?<value>"([^\"]|\.)*"|-?\d+|true|false|null)
*
* Behold the ability to capture matched braces with the .NET regex engine:
\{
(?>
[^{}]+
|
\{ (?<DEPTH>)
|
\} (?<-DEPTH>)
)*
(?(DEPTH)(?!))
\}
*/
//const string Value = ""([^\\"]|\\.)*"|-?\d+|true|false|null";
//const string ValueArray = "(?<array>\[)\s*(?<value>" + Value + ")(\s*,\s*(?<value>" + Value + "))*\s*\]";
//const string NameValueOrArray =
// ""(?<key>([^\\"]|\\.)*)"\s*" +
// ":\s*" +
// "((?<value>" + Value + ")|" + ValueArray + ")";
//const string Object = "^\s*{\s*" + NameValueOrArray + "(\s*,\s*" + NameValueOrArray + ")*\s*}\s*$";
const string String = ""([^\\"]|\\.)*"";
const string ObjectValue = "(?<object>\{)(?>\{(?<D>)|\}(?<-D>)|" + String + "|[^{}"]*)*(?(D)(?!))\}";
const string Value = String + @"|-?\d+.?\d*|true|false|null|" + ObjectValue;
//const string ValueArray = @"(?<array>[)\s*(?:(?<value>" + Value + @")(\s*,\s*(?<value>" + Value + @"))*)?\s*\]";
const string ValueArray = @"[\s*(?:(?:" + Value + @")(\s*,\s*(?:" + Value + @"))*)?\s*\]";
const string ValueOrArray = "(?:" + Value + "|" + ValueArray + ")";
const string ValueArray2 = @"(?<array>[)\s*(?:(?<value>" + ValueOrArray + @")(\s*,\s*(?<value>" + ValueOrArray + @"))*)?\s*\]";
const string NameValueOrArray =
""(?<key>([^\\"]|\\.)*)"\s*" +
":\s*" +
"((?<value>" + Value + ")|" + ValueArray2 + ")";
const string Object = "^\s*{\s*" + NameValueOrArray + "(\s*,\s*" + NameValueOrArray + ")*\s*}\s*$";
Dictionary<string, object> d = new Dictionary<string, object>();
Match m = Regex.Match(json, Object, RegexOptions.ExplicitCapture);
if (!m.Success)
throw new JsonException("Passed string (" + json + ") was not valid JSON.");
CaptureCollection keys = m.Groups["key"].Captures;
CaptureCollection values = m.Groups["value"].Captures;
CaptureCollection arrays = m.Groups["array"].Captures;
CaptureCollection objects = m.Groups["object"].Captures;
int j = 0;
int arrayIdx = 0;
int objectIdx = 0;
for (int i = 0; i < keys.Count; i++)
{
int nextKeyIdx = i + 1 < keys.Count ? keys[i + 1].Index : m.Length;
bool isArray = arrayIdx < arrays.Count && keys[i].Index < arrays[arrayIdx].Index && arrays[arrayIdx].Index < nextKeyIdx;
bool isObject = j < values.Count && values[j].Value.Length > 0 && values[j].Value.StartsWith("{");
List<object> array = null;
object o = null;
if (isArray)
{
array = new List<object>();
arrayIdx++;
}
if (isObject)
objectIdx++;
for (; j < values.Count && values[j].Index < nextKeyIdx; j++)
{
// we don't parse inner objects here; we let Parse(Type, string) do the recursion
object value = !isObject && (values[j].Value.Length == 0 || values[j].Value[0] != '[')
? ParseValue(values[j].Value)
: values[j].Value; // JsonBase.Parse(values[j].Value);
if (isArray)
array.Add(value);
else
o = value;
}
d.Add(keys[i].Value, isArray ? array.ToArray() : o);
}
return d;
}
protected static object ParseValue(string value)
{
if (value == "true")
return true;
if (value == "false")
return false;
if (value == "null")
return null;
if (value.StartsWith(""") && value.EndsWith("""))
return value.Substring(1, value.Length - 2);
byte by;
if (byte.TryParse(value, out by))
return by;
short n;
if (short.TryParse(value, out n))
return n;
int nn;
if (int.TryParse(value, out nn))
return nn;
double d;
if (double.TryParse(value, out d))
return d;
throw new JsonException("Unrecognized value: '" + value + "'.");
}
/// <summary>
/// Converts the object to JSON syntax.
/// </summary>
/// <returns>JSON representation.</returns>
public override string ToString()
{
Type t = this.GetType();
StringBuilder sb = new StringBuilder();
foreach (PropertyInfo pi in t.GetProperties())
{
object[] oa = pi.GetCustomAttributes(typeof(DefaultValueAttribute), false);
object o = pi.GetValue(this, null);
if (oa.Length == 1 && object.Equals(((DefaultValueAttribute)oa[0]).Value, o))
continue;
sb.Append((sb.Length > 0 ? ", " : "") + """ + pi.Name + "": ");
if (pi.PropertyType == typeof(bool))
{
sb.Append((bool)o ? "true" : "false");
}
else if (pi.PropertyType == typeof(string))
{
sb.Append(Stringify((string)o));
}
else if (
pi.PropertyType == typeof(byte) ||
pi.PropertyType == typeof(short) ||
pi.PropertyType == typeof(int) ||
pi.PropertyType == typeof(float) ||
pi.PropertyType == typeof(double) ||
pi.PropertyType.IsSubclassOf(typeof(JsonBase)))
{
sb.Append(o.ToString());
}
else if (pi.PropertyType.IsEnum)
{
sb.Append(Enum.Format(pi.PropertyType, o, "d"));
}
else if ((o as IEnumerable) != null)
{
AppendEnumerable(sb, (IEnumerable)o);
}
else
throw new JsonException("Cannot convert the type '" + pi.PropertyType.ToString() + "' to JSON.");
}
return "{" + sb + "}";
}
private string Stringify(string s)
{
if (s != null)
return """ + s.Replace("\", "\\").Replace(""", "\"").Replace("\n", "\n").Replace("\r", "\r") + """;
else
return "null";
}
private void AppendEnumerable(StringBuilder sb, IEnumerable ie)
{
bool addComma = false;
sb.Append('[');
foreach (object o in ie)
{
if (addComma)
sb.Append(", ");
addComma = true;
string s = o as string;
IEnumerable ie2 = null;
if (s == null)
ie2 = o as IEnumerable;
if (s != null)
sb.Append(Stringify(s));
else if (ie2 != null)
AppendEnumerable(sb, ie2);
else if (o != null)
sb.Append(o.ToString());
else
sb.Append("null");
}
sb.Append(']');
}
}
}