using System;
using Microsoft.SPOT;
using System.Reflection;
using System.Collections;
using IndianaJones.NETMF.String;
using IndianaJones.NETMF.Integer;
using IndianaJones.NETMF.Time;
namespace IndianaJones.NETMF.Json
{
///
/// .NET Micro Framework JSON Serializer and Deserializer.
/// Mimics, as closely as possible, the excellent JSON (de)serializer at http://www.json.org.
/// You can (de)serialize just about any object that contains real property values:
/// Value Types (int, bool, string, etc), Classes, Arrays, Dictionaries, Hashtables, etc.
/// Caveats:
/// 1) Each property to be (de)serialized must be public, and contain BOTH a property getter and setter.
/// 2) You can't (de)serialize interfaces, virtual or abstract properties, private properties.
/// Your class can contain these objects, but their values are not (de)serialized.
/// 3) DateTime objects can be (de)serialized, and their format in JSON will be ISO 8601 format.
/// 4) Guids can be (de)serialized.
/// 3) You can't use Array or IList because they are abstract (or an interface). Use ArrayList instead.
/// 4) You can't use IDictionaryEntry, use DictionaryEntry instead.
/// 5) .NET MF floating point seems to have very little precision, at least on my GHI USBizi hardware.
/// I get only about 3 or 4 decimal places of accuracy.
///
/// How does this class work, given the extremely limited Reflection capabilities?
/// Serialization is easy: you take the specified object, enumerate its Methods (yes, Methods, there
/// is no Property enumeration), and find the getters and setters, filtering out the unusable items.
/// Then you just JSON-format the Property names and their values.
///
/// Deserialization is much more difficult, as JSON contains no type definitions, and .NET MF contains
/// next-to-zero assistance. We instantiate a PropertyTable class to enumerate every public class
/// that's loaded by this process, and create a non-heirarchical Hashtable list of all public classes,
/// their property names, and each Property's Type. When the JSON string is presented for deserialization,
/// the JSON string is parsed (using this class) into a heirarchical Hashtable, containing all of the properties
/// and their values. At that point, you have (a) a Hastable containing Property Names with their Types,
/// and (b) a Hastable containing the Property Names and their actual Values. Deserialization at that point
/// is a matter of matching the Properties in the two lists, instantiating them by their Type, and setting
/// their Values. Hoping of course that you contain no classes with identical property names.
///
/// This is more difficult than it sounds. In normal .NET, you have boxing/unboxing to convert any object
/// to any defined Type. In .NET MF, you do not. The only Type that an object will unbox into is the
/// type exposed by the ReturnType property of the Parse method on the object. So a LOT of wrangling is
/// needed to properly unbox types from JSON into C#. But it (mostly) works.
///
///
/// Enumeration of the popular formats of time and date
/// within Json. It's not a standard, so you have to
/// know which on you're using.
///
public enum DateTimeFormat
{
Unknown = 0,
ISO8601 = 1,
Ajax = 2
}
///
/// .NET Micro Framework JSON Serializer and Deserializer.
///
public class Serializer
{
private PropertyTable _propertyTable;
private DateTimeFormat _dateFormat;
public Serializer()
{
_dateFormat = DateTimeFormat.ISO8601;
_propertyTable = new PropertyTable();
}
///
/// Gets/Sets the format that will be used to display
/// and parse dates in the Json data.
///
public DateTimeFormat DateFormat
{
get
{
return _dateFormat;
}
set
{
_dateFormat = value;
}
}
///
/// Serializes an object into a Json string.
///
///
///
public string Serialize(object o)
{
return JsonPrimitives.Serialize(o);
}
///
/// Desrializes a Json string into an object.
///
///
///
public object Deserialize(string json)
{
Hashtable table = JsonParser.JsonDecode(json) as Hashtable;
JsonPrimitives.DumpObjects(table, 0);
return _propertyTable.FindObject(table);
}
///
/// Resets the contents of the Property Table with current classes and property definitions.
/// This does NOT need to be called when values change, only when new classes are
/// loaded dynamically at runtime.
///
public void Snapshot()
{
_propertyTable.Snapshot();
}
}
public static class JsonPrimitives
{
///
/// Lookup table for hex values.
///
public const string ContentType = "application/json";
///
/// Deserializes the specified Json string into an object whose type matches
/// what was discovered in the PropertyTable.
///
///
///
public static object Deserialize(string json)
{
object result = null;
Hashtable table = JsonParser.JsonDecode(json) as Hashtable;
DumpObjects(table, 0);
return result;
}
#region Serialize Methods
///
/// Converts a character to its javascript unicode equivalent.
///
/// The character to convert.
/// The javascript unicode equivalent.
private static string JsUnicode(char c)
{
string HEX_CHARS = "0123456789ABCDEF";
string result = "\\u";
ushort value = (ushort)c;
for (int i = 0; i < 4; i++, value <<= 4)
{
result += HEX_CHARS[value >> 12];
}
return result;
}
///
/// Convert a value to JSON.
///
/// The value to convert. Supported types are: Boolean, String, Byte, (U)Int16, (U)Int32, Float, Double, Decimal, JsonObject, JsonArray, Array, Object and null.
/// The JSON object as a string or null when the value type is not supported.
/// For objects, only public fields are converted.
public static string Serialize(object o)
{
if (o == null)
return "null";
Type type = o.GetType();
// All ordinary value types and all objects that are classes that can
// and shouold be ToString()'d are handled here. Special objects like
// arrays and classes that have properties to be enumerated are handled below.
switch (type.Name)
{
case "Boolean":
{
return (bool)o ? "true" : "false";
}
case "String":
{
// Encapsulate object in double-quotes if it's not already
char v = o.ToString()[0];
if (v == '"')
{
return o.ToString();
}
else
{
return "\"" + o.ToString() + "\"";
}
}
case "Single":
case "Double":
case "Decimal":
case "Float":
{
return DoubleExtensions.ToString((double)o);
}
case "Byte":
case "SByte":
case "Int16":
case "UInt16":
case "Int32":
case "UInt32":
case "Int64":
case "UInt64":
case "JsonObject":
case "JsonArray":
{
return o.ToString();
}
case "Char":
case "Guid":
{
return "\"" + o.ToString() + "\"";
}
case "DateTime":
{
// This MSDN page describes the problem with JSON dates:
// http://msdn.microsoft.com/en-us/library/bb299886.aspx
return "\"" + DateTimeExtensions.ToIso8601((DateTime)o) + "\"";
}
}
if (type.IsArray)
{
JsonArray jsonArray = new JsonArray();
foreach (object i in (Array)o)
{
// If the array object needs to be serialized first, do it
object valueToAdd = string.Empty;
SerializeStatus serialize = GetSerializeState(i);
if (serialize == SerializeStatus.Serialize)
{
valueToAdd = Serialize(i);
}
else
{
valueToAdd = i;
}
jsonArray.Add(valueToAdd);
}
return jsonArray.ToString();
}
if (type == typeof(System.Collections.ArrayList))
{
JsonArray jsonArray = new JsonArray();
foreach (object i in (o as ArrayList))
{
// If the array object needs to be serialized first, do it
object valueToAdd = string.Empty;
SerializeStatus serialize = GetSerializeState(i);
if (serialize == SerializeStatus.Serialize)
{
valueToAdd = Serialize(i);
}
else
{
valueToAdd = i;
}
jsonArray.Add(valueToAdd);
}
return jsonArray.ToString();
}
if (type == typeof(System.Collections.Hashtable))
{
JsonObject main = new JsonObject();
Hashtable table = o as Hashtable;
JsonObject to = new JsonObject();
foreach (var key in table.Keys)
{
// If the array object needs to be serialized first, do it
object valueToAdd = string.Empty;
SerializeStatus serialize = GetSerializeState(table[key]);
if (serialize == SerializeStatus.Serialize)
{
valueToAdd = Serialize(table[key]);
}
else
{
valueToAdd = table[key];
}
to.Add(key, valueToAdd);
//to.Add(key, table[key]);
}
//main.Add(type.Name, to.ToString());
//return main.ToString();
return to.ToString();
}
if (type == typeof(System.Collections.DictionaryEntry))
{
DictionaryEntry dict = o as DictionaryEntry;
JsonObject to = new JsonObject();
// If the Value property of the DictionaryEntry is an object rather
// than a string, then serialize it first.
object valueToAdd = string.Empty;
SerializeStatus serialize = GetSerializeState(dict.Value);
if (serialize == SerializeStatus.Serialize)
{
valueToAdd = Serialize(dict.Value);
}
else
{
valueToAdd = dict.Value;
}
to.Add(dict.Key, valueToAdd);
return to.ToString();
}
if (type.IsClass)
{
JsonObject jsonObject = new JsonObject();
// Iterate through all of the methods, looking for GET properties
MethodInfo[] methods = type.GetMethods();
foreach (MethodInfo method in methods)
{
// We care only about property getters when serializing
if (method.Name.StartsWith("get_"))
{
// Ignore abstract and virtual objects
if ((method.IsAbstract ||
(method.IsVirtual) ||
(method.ReturnType.IsAbstract)))
{
continue;
}
// Ignore delegates and MethodInfos
if ((method.ReturnType == typeof(System.Delegate)) ||
(method.ReturnType == typeof(System.MulticastDelegate)) ||
(method.ReturnType == typeof(System.Reflection.MethodInfo)))
{
continue;
}
// Ditto for DeclaringType
if ((method.DeclaringType == typeof(System.Delegate)) ||
(method.DeclaringType == typeof(System.MulticastDelegate)))
{
continue;
}
// If the property returns a Hashtable
if (method.ReturnType == typeof(System.Collections.Hashtable))
{
Hashtable table = method.Invoke(o, null) as Hashtable;
JsonObject to = new JsonObject();
foreach (var key in table.Keys)
{
// If the array object needs to be serialized first, do it
object valueToAdd = string.Empty;
SerializeStatus serialize = GetSerializeState(table[key]);
if (serialize == SerializeStatus.Serialize)
{
valueToAdd = Serialize(table[key]);
}
else
{
valueToAdd = table[key];
}
to.Add(key, valueToAdd);
//to.Add(key, table[key]);
}
jsonObject.Add(method.Name.Substring(4), to.ToString());
continue;
}
// If the property returns an array of objects
if (method.ReturnType == typeof(System.Collections.ArrayList))
{
ArrayList no = method.Invoke(o, null) as ArrayList;
JsonArray jsonArray = new JsonArray();
foreach (object i in no)
{
// If the array object needs to be serialized first, do it
object valueToAdd = string.Empty;
SerializeStatus serialize = GetSerializeState(i);
if (serialize == SerializeStatus.Serialize)
{
valueToAdd = Serialize(i);
}
else
{
valueToAdd = i;
}
jsonArray.Add(valueToAdd);
}
jsonObject.Add(method.Name.Substring(4), jsonArray.ToString());
continue;
}
// If the property returns a DictionaryEntry
if (method.ReturnType == typeof(System.Collections.DictionaryEntry))
{
DictionaryEntry dict = method.Invoke(o, null) as DictionaryEntry;
// If the Value property of the DictionaryEntry needs to be serialized first, do it
object valueToAdd = string.Empty;
SerializeStatus serialize = GetSerializeState(dict.Value);
if (serialize == SerializeStatus.Serialize)
{
valueToAdd = Serialize(dict.Value);
}
else
{
valueToAdd = dict.Value;
}
// Wrap the DictionaryEntry in a JsonObject
JsonObject to = new JsonObject();
to.Add(dict.Key, valueToAdd);
jsonObject.Add(method.Name.Substring(4), to.ToString());
continue;
}
// If the property is a Class that should NOT be ToString()'d, because
// it has properties that must themselves be enumerated and serialized,
// then recursively call myself to serialize them.
if ((method.ReturnType.IsClass) &&
(method.ReturnType.IsArray == false) &&
(method.ReturnType.ToString().StartsWith("System.Collections") == false) &&
(method.ReturnType.ToString().StartsWith("System.String") == false))
{
object no = method.Invoke(o, null);
string value = Serialize(no);
jsonObject.Add(method.Name.Substring(4), value);
continue;
}
// All other properties are types that will be handled according to
// their type. That handler code is the switch statement at the top
// of this function.
object newo = method.Invoke(o, null);
jsonObject.Add(method.Name.Substring(4), newo);
}
}
return jsonObject.ToString();
}
return null;
}
public enum SerializeStatus
{
None = 0,
Serialize = 1
}
///
/// Determines if the specified object needs to be serialized. It needs to be serialized if it's a
/// class that contains properties that need enumeration. All other objects that can be directly
/// returned, such as ints, strings, etc, do not need to be serialized.
///
///
///
private static SerializeStatus GetSerializeState(object o)
{
Type type = o.GetType();
// Ignore delegates and MethodInfos
if ((type == typeof(System.Delegate)) ||
(type == typeof(System.MulticastDelegate)) ||
(type == typeof(System.Reflection.MethodInfo)))
{
return SerializeStatus.None;
}
// If the property returns a Hashtable
if (type == typeof(System.Collections.Hashtable))
{
return SerializeStatus.None;
}
// If the property returns an array of objects
if (type == typeof(System.Collections.ArrayList))
{
return SerializeStatus.Serialize;
}
// If the property returns a DictionaryEntry
if (type == typeof(System.Collections.DictionaryEntry))
{
return SerializeStatus.Serialize;
}
// If the property is a Class that should NOT be ToString()'d, because
// it has properties that must themselves be enumerated and serialized,
// then recursively call myself to serialize them.
if ((type.IsClass) &&
(type.IsArray == false) &&
(type.ToString().StartsWith("System.Collections") == false) &&
(type.ToString().StartsWith("System.String") == false))
{
return SerializeStatus.Serialize;
}
// All other properties are types that will be handled according to
// their type. That handler code is the switch statement at the top
// of this function.
return SerializeStatus.None;
}
#endregion
#region Deserialize Methods
public static void DumpObjects(Hashtable hash, int level)
{
foreach (DictionaryEntry d in hash)
{
string name = d.Key.ToString();
string value = string.Empty;
string tabs = string.Empty;
for (int i = 0; i < level; i++)
{
tabs = tabs + " ";
}
Debug.Print(tabs + name + " : ");
if (d.Value is Hashtable)
{
DumpObjects(d.Value as Hashtable, level + 4);
}
if (d.Value is ArrayList)
{
DumpObjectArray(d.Value as ArrayList, level + 4);
}
else
{
Debug.Print(d.Value.ToString());
}
}
}
private static void DumpObjectArray(ArrayList array, int level)
{
foreach (object o in array)
{
if (o is Hashtable)
{
DumpObjects(o as Hashtable, level + 4);
}
else if (o is ArrayList)
{
DumpObjectArray(o as ArrayList, level + 4);
}
else
{
Debug.Print(o.ToString());
}
}
}
#endregion
}
}