tsunami

log in
history

JsonBase

Luke Breuer
2008-04-24 21:26 UTC

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&lt;T&gt; 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 &quot; 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 &quot; -- 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(']');
        }
    } 
}