diff --git a/JSON.cs b/JSON.cs new file mode 100644 index 0000000..25046e4 --- /dev/null +++ b/JSON.cs @@ -0,0 +1,486 @@ +using System; +using System.Collections; +using System.Globalization; +using System.Text; + +namespace Procurios.Public +{ + /// + /// This class encodes and decodes JSON strings. + /// Spec. details, see http://www.json.org/ + /// + /// JSON uses Arrays and Objects. These correspond here to the datatypes ArrayList and Hashtable. + /// All numbers are parsed to doubles. + /// + public class JSON + { + public const int TOKEN_NONE = 0; + public const int TOKEN_CURLY_OPEN = 1; + public const int TOKEN_CURLY_CLOSE = 2; + public const int TOKEN_SQUARED_OPEN = 3; + public const int TOKEN_SQUARED_CLOSE = 4; + public const int TOKEN_COLON = 5; + public const int TOKEN_COMMA = 6; + public const int TOKEN_STRING = 7; + public const int TOKEN_NUMBER = 8; + public const int TOKEN_TRUE = 9; + public const int TOKEN_FALSE = 10; + public const int TOKEN_NULL = 11; + + private const int BUILDER_CAPACITY = 2000; + + /// + /// Parses the string json into a value + /// + /// A JSON string. + /// An ArrayList, a Hashtable, a double, a string, null, true, or false + public static object JsonDecode(string json) + { + bool success = true; + + return JsonDecode(json, ref success); + } + + /// + /// Parses the string json into a value; and fills 'success' with the successfullness of the parse. + /// + /// A JSON string. + /// Successful parse? + /// An ArrayList, a Hashtable, a double, a string, null, true, or false + public static object JsonDecode(string json, ref bool success) + { + success = true; + if (json != null) { + char[] charArray = json.ToCharArray(); + int index = 0; + object value = ParseValue(charArray, ref index, ref success); + return value; + } else { + return null; + } + } + + /// + /// Converts a Hashtable / ArrayList object into a JSON string + /// + /// A Hashtable / ArrayList + /// A JSON encoded string, or null if object 'json' is not serializable + public static string JsonEncode(object json) + { + StringBuilder builder = new StringBuilder(BUILDER_CAPACITY); + bool success = SerializeValue(json, builder); + return (success ? builder.ToString() : null); + } + + protected static Hashtable ParseObject(char[] json, ref int index, ref bool success) + { + Hashtable table = new Hashtable(); + int token; + + // { + NextToken(json, ref index); + + bool done = false; + while (!done) { + token = LookAhead(json, index); + if (token == JSON.TOKEN_NONE) { + success = false; + return null; + } else if (token == JSON.TOKEN_COMMA) { + NextToken(json, ref index); + } else if (token == JSON.TOKEN_CURLY_CLOSE) { + NextToken(json, ref index); + return table; + } else { + + // name + string name = ParseString(json, ref index, ref success); + if (!success) { + success = false; + return null; + } + + // : + token = NextToken(json, ref index); + if (token != JSON.TOKEN_COLON) { + success = false; + return null; + } + + // value + object value = ParseValue(json, ref index, ref success); + if (!success) { + success = false; + return null; + } + + table[name] = value; + } + } + + return table; + } + + protected static ArrayList ParseArray(char[] json, ref int index, ref bool success) + { + ArrayList array = new ArrayList(); + + // [ + NextToken(json, ref index); + + bool done = false; + while (!done) { + int token = LookAhead(json, index); + if (token == JSON.TOKEN_NONE) { + success = false; + return null; + } else if (token == JSON.TOKEN_COMMA) { + NextToken(json, ref index); + } else if (token == JSON.TOKEN_SQUARED_CLOSE) { + NextToken(json, ref index); + break; + } else { + object value = ParseValue(json, ref index, ref success); + if (!success) { + return null; + } + + array.Add(value); + } + } + + return array; + } + + protected static object ParseValue(char[] json, ref int index, ref bool success) + { + switch (LookAhead(json, index)) { + case JSON.TOKEN_STRING: + return ParseString(json, ref index, ref success); + case JSON.TOKEN_NUMBER: + return ParseNumber(json, ref index, ref success); + case JSON.TOKEN_CURLY_OPEN: + return ParseObject(json, ref index, ref success); + case JSON.TOKEN_SQUARED_OPEN: + return ParseArray(json, ref index, ref success); + case JSON.TOKEN_TRUE: + NextToken(json, ref index); + return true; + case JSON.TOKEN_FALSE: + NextToken(json, ref index); + return false; + case JSON.TOKEN_NULL: + NextToken(json, ref index); + return null; + case JSON.TOKEN_NONE: + break; + } + + success = false; + return null; + } + + protected static string ParseString(char[] json, ref int index, ref bool success) + { + StringBuilder s = new StringBuilder(BUILDER_CAPACITY); + char c; + + EatWhitespace(json, ref index); + + // " + c = json[index++]; + + bool complete = false; + while (!complete) { + + if (index == json.Length) { + break; + } + + c = json[index++]; + if (c == '"') { + complete = true; + break; + } else if (c == '\\') { + + if (index == json.Length) { + break; + } + c = json[index++]; + if (c == '"') { + s.Append('"'); + } else if (c == '\\') { + s.Append('\\'); + } else if (c == '/') { + s.Append('/'); + } else if (c == 'b') { + s.Append('\b'); + } else if (c == 'f') { + s.Append('\f'); + } else if (c == 'n') { + s.Append('\n'); + } else if (c == 'r') { + s.Append('\r'); + } else if (c == 't') { + s.Append('\t'); + } else if (c == 'u') { + int remainingLength = json.Length - index; + if (remainingLength >= 4) { + // parse the 32 bit hex into an integer codepoint + uint codePoint; + if (!(success = UInt32.TryParse(new string(json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint))) { + return ""; + } + // convert the integer codepoint to a unicode char and add to string + s.Append(Char.ConvertFromUtf32((int)codePoint)); + // skip 4 chars + index += 4; + } else { + break; + } + } + + } else { + s.Append(c); + } + + } + + if (!complete) { + success = false; + return null; + } + + return s.ToString(); + } + + protected static double ParseNumber(char[] json, ref int index, ref bool success) + { + EatWhitespace(json, ref index); + + int lastIndex = GetLastIndexOfNumber(json, index); + int charLength = (lastIndex - index) + 1; + + double number; + success = Double.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); + + index = lastIndex + 1; + return number; + } + + protected static int GetLastIndexOfNumber(char[] json, int index) + { + int lastIndex; + + for (lastIndex = index; lastIndex < json.Length; lastIndex++) { + if ("0123456789+-.eE".IndexOf(json[lastIndex]) == -1) { + break; + } + } + return lastIndex - 1; + } + + protected static void EatWhitespace(char[] json, ref int index) + { + for (; index < json.Length; index++) { + if (" \t\n\r".IndexOf(json[index]) == -1) { + break; + } + } + } + + protected static int LookAhead(char[] json, int index) + { + int saveIndex = index; + return NextToken(json, ref saveIndex); + } + + protected static int NextToken(char[] json, ref int index) + { + EatWhitespace(json, ref index); + + if (index == json.Length) { + return JSON.TOKEN_NONE; + } + + char c = json[index]; + index++; + switch (c) { + case '{': + return JSON.TOKEN_CURLY_OPEN; + case '}': + return JSON.TOKEN_CURLY_CLOSE; + case '[': + return JSON.TOKEN_SQUARED_OPEN; + case ']': + return JSON.TOKEN_SQUARED_CLOSE; + case ',': + return JSON.TOKEN_COMMA; + case '"': + return JSON.TOKEN_STRING; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '-': + return JSON.TOKEN_NUMBER; + case ':': + return JSON.TOKEN_COLON; + } + index--; + + int remainingLength = json.Length - index; + + // false + if (remainingLength >= 5) { + if (json[index] == 'f' && + json[index + 1] == 'a' && + json[index + 2] == 'l' && + json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return JSON.TOKEN_FALSE; + } + } + + // true + if (remainingLength >= 4) { + if (json[index] == 't' && + json[index + 1] == 'r' && + json[index + 2] == 'u' && + json[index + 3] == 'e') { + index += 4; + return JSON.TOKEN_TRUE; + } + } + + // null + if (remainingLength >= 4) { + if (json[index] == 'n' && + json[index + 1] == 'u' && + json[index + 2] == 'l' && + json[index + 3] == 'l') { + index += 4; + return JSON.TOKEN_NULL; + } + } + + return JSON.TOKEN_NONE; + } + + protected static bool SerializeValue(object value, StringBuilder builder) + { + bool success = true; + + if (value is string) { + success = SerializeString((string)value, builder); + } else if (value is Hashtable) { + success = SerializeObject((Hashtable)value, builder); + } else if (value is ArrayList) { + success = SerializeArray((ArrayList)value, builder); + } else if ((value is Boolean) && ((Boolean)value == true)) { + builder.Append("true"); + } else if ((value is Boolean) && ((Boolean)value == false)) { + builder.Append("false"); + } else if (value is ValueType) { + // thanks to ritchie for pointing out ValueType to me + success = SerializeNumber(Convert.ToDouble(value), builder); + } else if (value == null) { + builder.Append("null"); + } else { + success = false; + } + return success; + } + + protected static bool SerializeObject(Hashtable anObject, StringBuilder builder) + { + builder.Append("{"); + + IDictionaryEnumerator e = anObject.GetEnumerator(); + bool first = true; + while (e.MoveNext()) { + string key = e.Key.ToString(); + object value = e.Value; + + if (!first) { + builder.Append(", "); + } + + SerializeString(key, builder); + builder.Append(":"); + if (!SerializeValue(value, builder)) { + return false; + } + + first = false; + } + + builder.Append("}"); + return true; + } + + protected static bool SerializeArray(ArrayList anArray, StringBuilder builder) + { + builder.Append("["); + + bool first = true; + for (int i = 0; i < anArray.Count; i++) { + object value = anArray[i]; + + if (!first) { + builder.Append(", "); + } + + if (!SerializeValue(value, builder)) { + return false; + } + + first = false; + } + + builder.Append("]"); + return true; + } + + protected static bool SerializeString(string aString, StringBuilder builder) + { + builder.Append("\""); + + char[] charArray = aString.ToCharArray(); + for (int i = 0; i < charArray.Length; i++) { + char c = charArray[i]; + if (c == '"') { + builder.Append("\\\""); + } else if (c == '\\') { + builder.Append("\\\\"); + } else if (c == '\b') { + builder.Append("\\b"); + } else if (c == '\f') { + builder.Append("\\f"); + } else if (c == '\n') { + builder.Append("\\n"); + } else if (c == '\r') { + builder.Append("\\r"); + } else if (c == '\t') { + builder.Append("\\t"); + } else { + int codepoint = Convert.ToInt32(c); + if ((codepoint >= 32) && (codepoint <= 126)) { + builder.Append(c); + } else { + builder.Append("\\u" + Convert.ToString(codepoint, 16).PadLeft(4, '0')); + } + } + } + + builder.Append("\""); + return true; + } + + protected static bool SerializeNumber(double number, StringBuilder builder) + { + builder.Append(Convert.ToString(number, CultureInfo.InvariantCulture)); + return true; + } + } +} +