500 lines
14 KiB
C#
500 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
|
|
namespace Scrummer.Code.JSON
|
|
{
|
|
public class JSONParser
|
|
{
|
|
#region Declarations
|
|
|
|
private ParserContext ctx;
|
|
private bool tainted = false;
|
|
|
|
private List<Type> _knownTypes = new List<Type>();
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public bool Tainted
|
|
{
|
|
get { return tainted; }
|
|
}
|
|
|
|
public List<Type> KnownTypes
|
|
{
|
|
get { return _knownTypes; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private methods
|
|
|
|
private static Dictionary<Type, PropertyInfo[]> _dictProperties = new Dictionary<Type, PropertyInfo[]>();
|
|
|
|
private PropertyInfo[] Type_GetProperties(Type type)
|
|
{
|
|
PropertyInfo[] typeProperties = null;
|
|
if (_dictProperties.ContainsKey(type)) { typeProperties = _dictProperties[type]; }
|
|
else
|
|
{
|
|
lock(_dictProperties){
|
|
|
|
if (_dictProperties.ContainsKey(type)) { typeProperties = _dictProperties[type]; }
|
|
else
|
|
{
|
|
typeProperties = type.GetProperties(BindingFlags.Public | BindingFlags.OptionalParamBinding | BindingFlags.Instance);
|
|
_dictProperties.Add(type, typeProperties);
|
|
}
|
|
}
|
|
}
|
|
return typeProperties;
|
|
}
|
|
|
|
private float CompareToType(Dictionary<string, object> obj, Type type)
|
|
{
|
|
PropertyInfo[] typeProperties = Type_GetProperties(type);
|
|
int count = 0;
|
|
foreach (PropertyInfo prop in typeProperties)
|
|
{
|
|
if (obj.ContainsKey(prop.Name))
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
return ((float)count / (float)typeProperties.Length);
|
|
}
|
|
|
|
private object ConvertToType(Dictionary<string, object> obj, Type type)
|
|
{
|
|
PropertyInfo[] typeProperties = Type_GetProperties(type);
|
|
object newObj = Activator.CreateInstance(type);
|
|
foreach (PropertyInfo prop in typeProperties)
|
|
{
|
|
if (obj.ContainsKey(prop.Name))
|
|
{
|
|
prop.SetValue(newObj, Convert.ChangeType(obj[prop.Name], prop.PropertyType), null);
|
|
}
|
|
}
|
|
return newObj;
|
|
}
|
|
|
|
private object TryConvertToTypes(Dictionary<string, object> obj)
|
|
{
|
|
Type bestMatch = null;
|
|
float bestMatchFactor = 0.0f;
|
|
foreach (Type type in _knownTypes)
|
|
{
|
|
float matchFactor = CompareToType(obj, type);
|
|
if (matchFactor > bestMatchFactor)
|
|
{
|
|
bestMatch = type;
|
|
bestMatchFactor = matchFactor;
|
|
}
|
|
}
|
|
if (bestMatch != null)
|
|
{
|
|
return ConvertToType(obj, bestMatch);
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
private int ParseHexShort()
|
|
{
|
|
int value = 0;
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
char c = ctx.Next();
|
|
if (Char.IsDigit(c))
|
|
{
|
|
value = (value << 4) | (c - '0');
|
|
}
|
|
else
|
|
{
|
|
c = Char.ToLower(c);
|
|
if (c >= 'a' && c <= 'f')
|
|
{
|
|
value = (value << 4) | ((c - 'a') + 10);
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private String ParseQuotedString()
|
|
{
|
|
StringBuilder scratch = new StringBuilder();
|
|
char c = ctx.SkipWhite();
|
|
if (c == '"')
|
|
{
|
|
c = ctx.Next();
|
|
}
|
|
do
|
|
{
|
|
if (c == '\\')
|
|
{
|
|
c = ctx.Next();
|
|
if (c == '"')
|
|
{
|
|
scratch.Append('"');
|
|
}
|
|
else if (c == '\\')
|
|
{
|
|
scratch.Append('\\');
|
|
}
|
|
else if (c == '/')
|
|
{
|
|
scratch.Append('/');
|
|
}
|
|
else if (c == 'b')
|
|
{
|
|
scratch.Append('\b');
|
|
}
|
|
else if (c == 'f')
|
|
{
|
|
scratch.Append('\f');
|
|
}
|
|
else if (c == 'n')
|
|
{
|
|
scratch.Append('\n');
|
|
}
|
|
else if (c == 'r')
|
|
{
|
|
scratch.Append('\r');
|
|
}
|
|
else if (c == 't')
|
|
{
|
|
scratch.Append('\t');
|
|
}
|
|
else if (c == 'u')
|
|
{
|
|
scratch.Append((char)ParseHexShort());
|
|
}
|
|
c = ctx.Next();
|
|
}
|
|
else if (c == '"')
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
scratch.Append(c);
|
|
c = ctx.Next();
|
|
}
|
|
} while (!ctx.AtEnd());
|
|
if (c == '"')
|
|
{
|
|
ctx.Next();
|
|
}
|
|
return scratch.ToString();
|
|
}
|
|
|
|
private String ParseString()
|
|
{
|
|
char c = ctx.SkipWhite();
|
|
if (c == '"')
|
|
{
|
|
return ParseQuotedString();
|
|
}
|
|
StringBuilder scratch = new StringBuilder();
|
|
|
|
while (!ctx.AtEnd()
|
|
&& (Char.IsLetter(c) || Char.IsDigit(c) || c == '_'))
|
|
{
|
|
scratch.Append(c);
|
|
c = ctx.Next();
|
|
}
|
|
|
|
return scratch.ToString();
|
|
}
|
|
|
|
private Object ParseNumber()
|
|
{
|
|
StringBuilder scratch = new StringBuilder();
|
|
bool isFloat = false;
|
|
int numberLenght = 0;
|
|
char c;
|
|
c = ctx.SkipWhite();
|
|
|
|
// Sign
|
|
if (c == '-')
|
|
{
|
|
scratch.Append('-');
|
|
c = ctx.Next();
|
|
}
|
|
|
|
// Integer part
|
|
while (Char.IsDigit(c))
|
|
{
|
|
scratch.Append(c);
|
|
c = ctx.Next();
|
|
numberLenght++;
|
|
}
|
|
|
|
// Decimal part
|
|
if (c == '.')
|
|
{
|
|
isFloat = true;
|
|
scratch.Append('.');
|
|
c = ctx.Next();
|
|
while (Char.IsDigit(c))
|
|
{
|
|
scratch.Append(c);
|
|
c = ctx.Next();
|
|
numberLenght++;
|
|
}
|
|
}
|
|
|
|
if (numberLenght == 0)
|
|
{
|
|
tainted = true;
|
|
return null;
|
|
}
|
|
|
|
// Exponential part
|
|
if (c == 'e' || c == 'E')
|
|
{
|
|
isFloat = true;
|
|
scratch.Append('E');
|
|
c = ctx.Next();
|
|
if (c == '+' || c == '-')
|
|
{
|
|
scratch.Append(c);
|
|
}
|
|
while (Char.IsDigit(c))
|
|
{
|
|
scratch.Append(c);
|
|
c = ctx.Next();
|
|
numberLenght++;
|
|
}
|
|
}
|
|
|
|
// Build number object from the parsed string
|
|
String s = scratch.ToString();
|
|
return isFloat ? (numberLenght < 17) ? (Object)Double.Parse(s)
|
|
: Decimal.Parse(s) : (numberLenght < 19) ? (Object)System.Int32.Parse(s)
|
|
: (Object)System.Int32.Parse(s);
|
|
}
|
|
|
|
private List<object> ParseArray()
|
|
{
|
|
char c = ctx.SkipWhite();
|
|
List<object> array = new List<object>();
|
|
if (c == '[')
|
|
{
|
|
ctx.Next();
|
|
}
|
|
do
|
|
{
|
|
c = ctx.SkipWhite();
|
|
if (c == ']')
|
|
{
|
|
ctx.Next();
|
|
break;
|
|
}
|
|
else if (c == ',')
|
|
{
|
|
ctx.Next();
|
|
}
|
|
else
|
|
{
|
|
array.Add(ParseValue());
|
|
}
|
|
} while (!ctx.AtEnd());
|
|
return array;
|
|
}
|
|
|
|
private Dictionary<string, object> ParseObject()
|
|
{
|
|
char c = ctx.SkipWhite();
|
|
Dictionary<string, object> obj = new Dictionary<string, object>();
|
|
if (c == '{')
|
|
{
|
|
ctx.Next();
|
|
c = ctx.SkipWhite();
|
|
}
|
|
String attributeName;
|
|
Object attributeValue;
|
|
do
|
|
{
|
|
attributeName = ParseString();
|
|
c = ctx.SkipWhite();
|
|
if (c == ':')
|
|
{
|
|
ctx.Next();
|
|
attributeValue = ParseValue();
|
|
if (attributeName.Length > 0)
|
|
{
|
|
obj.Add(attributeName, attributeValue);
|
|
}
|
|
}
|
|
else if (c == ',')
|
|
{
|
|
ctx.Next();
|
|
c = ctx.SkipWhite();
|
|
}
|
|
else if (c == '}')
|
|
{
|
|
ctx.Next();
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// Unexpected character
|
|
tainted = true;
|
|
break;
|
|
}
|
|
} while (!ctx.AtEnd());
|
|
if (obj.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
private Object ParseValue()
|
|
{
|
|
Object token = null;
|
|
char c = ctx.SkipWhite();
|
|
switch (c)
|
|
{
|
|
case '"':
|
|
token = ParseQuotedString();
|
|
break;
|
|
case '{':
|
|
Dictionary<string, object> obj = ParseObject();
|
|
token = TryConvertToTypes(obj);
|
|
break;
|
|
case '[':
|
|
token = ParseArray();
|
|
break;
|
|
default:
|
|
if (Char.IsDigit(c) || c == '-')
|
|
{
|
|
token = ParseNumber();
|
|
}
|
|
else
|
|
{
|
|
String aux = ParseString();
|
|
if (aux.CompareTo("true") == 0)
|
|
{
|
|
token = true;
|
|
}
|
|
else if (aux.CompareTo("false") == 0)
|
|
{
|
|
token = false;
|
|
}
|
|
else if (aux.CompareTo("null") == 0)
|
|
{
|
|
token = null;
|
|
}
|
|
else
|
|
{
|
|
// Unexpected string
|
|
if (aux.Length == 0)
|
|
{
|
|
ctx.Next();
|
|
}
|
|
tainted = true;
|
|
token = null;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return token;
|
|
}
|
|
|
|
private String CleanIdentifier(String input)
|
|
{
|
|
int i;
|
|
char c;
|
|
i = input.Length - 1;
|
|
if (i < 0)
|
|
{
|
|
return input;
|
|
}
|
|
c = input[i];
|
|
while (Char.IsLetter(c) || Char.IsDigit(c) || c == '_')
|
|
{
|
|
i--;
|
|
if (i < 0)
|
|
{
|
|
break;
|
|
}
|
|
c = input[i];
|
|
}
|
|
return input.Substring(i + 1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public methods
|
|
|
|
public Object Parse(String text)
|
|
{
|
|
// Get the first object
|
|
ctx = new ParserContext(text);
|
|
tainted = false;
|
|
ctx.Mark();
|
|
Object obj = ParseValue();
|
|
if (ctx.AtEnd())
|
|
{
|
|
return obj;
|
|
}
|
|
|
|
// "But wait, there is more!"
|
|
int idx = 0;
|
|
String name = "";
|
|
String strInvalidPrev = "";
|
|
Dictionary<string, object> superObject = new Dictionary<string, object>();
|
|
do
|
|
{
|
|
// Add the object to the superObject
|
|
if (!tainted && name.Length > 0 && obj != null)
|
|
{
|
|
if (name.Length == 0)
|
|
{
|
|
name = String.Format("{0:D2}", idx);
|
|
}
|
|
superObject.Add(name, obj);
|
|
idx++;
|
|
name = "";
|
|
}
|
|
else
|
|
{
|
|
String strInvalid = ctx.GetMarked();
|
|
strInvalid = strInvalid.Trim();
|
|
if (strInvalidPrev.Length > 0
|
|
&& "=".CompareTo(strInvalid) == 0)
|
|
{
|
|
name = CleanIdentifier(strInvalidPrev);
|
|
}
|
|
else
|
|
{
|
|
name = "";
|
|
}
|
|
strInvalidPrev = strInvalid;
|
|
}
|
|
|
|
// Check end
|
|
if (ctx.AtEnd())
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Get next object
|
|
tainted = false;
|
|
ctx.Mark();
|
|
obj = ParseValue();
|
|
|
|
} while (true);
|
|
return superObject;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|