// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace Tools.DotNETCommon { /// /// Exception thrown for errors parsing JSON files /// public class JsonParseException : Exception { /// /// Constructor /// /// Format string /// Optional arguments public JsonParseException(string Format, params object[] Args) : base(String.Format(Format, Args)) { } } /// /// Stores a JSON object in memory /// public class JsonObject { Dictionary RawObject; /// /// Construct a JSON object from the raw string -> object dictionary /// /// Raw object parsed from disk public JsonObject(Dictionary InRawObject) { RawObject = new Dictionary(InRawObject, StringComparer.InvariantCultureIgnoreCase); } /// /// Read a JSON file from disk and construct a JsonObject from it /// /// File to read from /// New JsonObject instance public static JsonObject Read(FileReference File) { string Text = FileReference.ReadAllText(File); try { return Parse(Text); } catch(Exception Ex) { throw new JsonParseException("Unable to parse {0}: {1}", File, Ex.Message); } } /// /// Tries to read a JSON file from disk /// /// File to read from /// On success, receives the parsed object /// True if the file was read, false otherwise public static bool TryRead(FileReference FileName, out JsonObject Result) { if (!FileReference.Exists(FileName)) { Result = null; return false; } string Text = FileReference.ReadAllText(FileName); return TryParse(Text, out Result); } /// /// Parse a JsonObject from the given raw text string /// /// The text to parse /// New JsonObject instance public static JsonObject Parse(string Text) { Dictionary CaseSensitiveRawObject = (Dictionary)fastJSON.JSON.Instance.Parse(Text); return new JsonObject(CaseSensitiveRawObject); } /// /// Try to parse a JsonObject from the given raw text string /// /// The text to parse /// On success, receives the new JsonObject /// True if the object was parsed public static bool TryParse(string Text, out JsonObject Result) { try { Result = Parse(Text); return true; } catch (Exception) { Result = null; return false; } } /// /// List of key names in this object /// public IEnumerable KeyNames { get { return RawObject.Keys; } } /// /// Gets a string field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public string GetStringField(string FieldName) { string StringValue; if (!TryGetStringField(FieldName, out StringValue)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return StringValue; } /// /// Tries to read a string field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetStringField(string FieldName, out string Result) { object RawValue; if (RawObject.TryGetValue(FieldName, out RawValue) && (RawValue is string)) { Result = (string)RawValue; return true; } else { Result = null; return false; } } /// /// Gets a string array field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public string[] GetStringArrayField(string FieldName) { string[] StringValues; if (!TryGetStringArrayField(FieldName, out StringValues)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return StringValues; } /// /// Tries to read a string array field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetStringArrayField(string FieldName, out string[] Result) { object RawValue; if (RawObject.TryGetValue(FieldName, out RawValue) && (RawValue is IEnumerable) && ((IEnumerable)RawValue).All(x => x is string)) { Result = ((IEnumerable)RawValue).Select(x => (string)x).ToArray(); return true; } else { Result = null; return false; } } /// /// Gets a boolean field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public bool GetBoolField(string FieldName) { bool BoolValue; if (!TryGetBoolField(FieldName, out BoolValue)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return BoolValue; } /// /// Tries to read a bool field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetBoolField(string FieldName, out bool Result) { object RawValue; if (RawObject.TryGetValue(FieldName, out RawValue) && (RawValue is Boolean)) { Result = (bool)RawValue; return true; } else { Result = false; return false; } } /// /// Gets an integer field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public int GetIntegerField(string FieldName) { int IntegerValue; if (!TryGetIntegerField(FieldName, out IntegerValue)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return IntegerValue; } /// /// Tries to read an integer field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetIntegerField(string FieldName, out int Result) { object RawValue; if (!RawObject.TryGetValue(FieldName, out RawValue) || !int.TryParse(RawValue.ToString(), out Result)) { Result = 0; return false; } return true; } /// /// Tries to read an unsigned integer field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetUnsignedIntegerField(string FieldName, out uint Result) { object RawValue; if (!RawObject.TryGetValue(FieldName, out RawValue) || !uint.TryParse(RawValue.ToString(), out Result)) { Result = 0; return false; } return true; } /// /// Gets a double field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public double GetDoubleField(string FieldName) { double DoubleValue; if (!TryGetDoubleField(FieldName, out DoubleValue)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return DoubleValue; } /// /// Tries to read a double field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetDoubleField(string FieldName, out double Result) { object RawValue; if (!RawObject.TryGetValue(FieldName, out RawValue) || !double.TryParse(RawValue.ToString(), out Result)) { Result = 0.0; return false; } return true; } /// /// Gets an enum field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public T GetEnumField(string FieldName) where T : struct { T EnumValue; if (!TryGetEnumField(FieldName, out EnumValue)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return EnumValue; } /// /// Tries to read an enum field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetEnumField(string FieldName, out T Result) where T : struct { string StringValue; if (!TryGetStringField(FieldName, out StringValue) || !Enum.TryParse(StringValue, true, out Result)) { Result = default(T); return false; } return true; } /// /// Tries to read an enum array field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetEnumArrayField(string FieldName, out T[] Result) where T : struct { string[] StringValues; if (!TryGetStringArrayField(FieldName, out StringValues)) { Result = null; return false; } T[] EnumValues = new T[StringValues.Length]; for (int Idx = 0; Idx < StringValues.Length; Idx++) { if (!Enum.TryParse(StringValues[Idx], true, out EnumValues[Idx])) { Result = null; return false; } } Result = EnumValues; return true; } /// /// Gets an object field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public JsonObject GetObjectField(string FieldName) { JsonObject Result; if (!TryGetObjectField(FieldName, out Result)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return Result; } /// /// Tries to read an object field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetObjectField(string FieldName, out JsonObject Result) { object RawValue; if (RawObject.TryGetValue(FieldName, out RawValue) && (RawValue is Dictionary)) { Result = new JsonObject((Dictionary)RawValue); return true; } else { Result = null; return false; } } /// /// Gets an object array field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public JsonObject[] GetObjectArrayField(string FieldName) { JsonObject[] Result; if (!TryGetObjectArrayField(FieldName, out Result)) { throw new JsonParseException("Missing or invalid '{0}' field", FieldName); } return Result; } /// /// Tries to read an object array field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetObjectArrayField(string FieldName, out JsonObject[] Result) { object RawValue; if (RawObject.TryGetValue(FieldName, out RawValue) && (RawValue is IEnumerable) && ((IEnumerable)RawValue).All(x => x is Dictionary)) { Result = ((IEnumerable)RawValue).Select(x => new JsonObject((Dictionary)x)).ToArray(); return true; } else { Result = null; return false; } } } /// /// Writer for JSON data, which indents the output text appropriately, and adds commas and newlines between fields /// public class JsonWriter : IDisposable { TextWriter Writer; bool bLeaveOpen; bool bRequiresComma; string Indent; /// /// Constructor /// /// File to write to public JsonWriter(string FileName) : this(new StreamWriter(FileName)) { } /// /// Constructor /// /// File to write to public JsonWriter(FileReference FileName) : this(new StreamWriter(FileName.FullName)) { } /// /// Constructor /// /// The text writer to output to /// Whether to leave the writer open when the object is disposed public JsonWriter(TextWriter Writer, bool bLeaveOpen = false) { this.Writer = Writer; this.bLeaveOpen = bLeaveOpen; Indent = ""; } /// /// Dispose of any managed resources /// public void Dispose() { if(!bLeaveOpen && Writer != null) { Writer.Dispose(); Writer = null; } } /// /// Write the opening brace for an object /// public void WriteObjectStart() { WriteCommaNewline(); Writer.Write(Indent); Writer.Write("{"); Indent += "\t"; bRequiresComma = false; } /// /// Write the name and opening brace for an object /// /// Name of the field public void WriteObjectStart(string ObjectName) { WriteCommaNewline(); Writer.Write("{0}\"{1}\": ", Indent, ObjectName); bRequiresComma = false; WriteObjectStart(); } /// /// Write the closing brace for an object /// public void WriteObjectEnd() { Indent = Indent.Substring(0, Indent.Length - 1); Writer.WriteLine(); Writer.Write(Indent); Writer.Write("}"); bRequiresComma = true; } /// /// Write the name and opening bracket for an array /// /// Name of the field public void WriteArrayStart(string ArrayName) { WriteCommaNewline(); Writer.Write("{0}\"{1}\": [", Indent, ArrayName); Indent += "\t"; bRequiresComma = false; } /// /// Write the closing bracket for an array /// public void WriteArrayEnd() { Indent = Indent.Substring(0, Indent.Length - 1); Writer.WriteLine(); Writer.Write("{0}]", Indent); bRequiresComma = true; } /// /// Write an array of strings /// /// Name of the field /// Values for the field public void WriteStringArrayField(string Name, IEnumerable Values) { WriteArrayStart(Name); foreach(string Value in Values) { WriteValue(Value); } WriteArrayEnd(); } /// /// Write an array of enum values /// /// Name of the field /// Values for the field public void WriteEnumArrayField(string Name, IEnumerable Values) where T : struct { WriteStringArrayField(Name, Values.Select(x => x.ToString())); } /// /// Write a value with no field name, for the contents of an array /// /// Value to write public void WriteValue(string Value) { WriteCommaNewline(); Writer.Write(Indent); WriteEscapedString(Value); bRequiresComma = true; } /// /// Write a field name and string value /// /// Name of the field /// Value for the field public void WriteValue(string Name, string Value) { WriteCommaNewline(); Writer.Write("{0}\"{1}\": ", Indent, Name); WriteEscapedString(Value); bRequiresComma = true; } /// /// Write a field name and integer value /// /// Name of the field /// Value for the field public void WriteValue(string Name, int Value) { WriteValueInternal(Name, Value.ToString()); } /// /// Write a field name and double value /// /// Name of the field /// Value for the field public void WriteValue(string Name, double Value) { WriteValueInternal(Name, Value.ToString()); } /// /// Write a field name and bool value /// /// Name of the field /// Value for the field public void WriteValue(string Name, bool Value) { WriteValueInternal(Name, Value ? "true" : "false"); } void WriteCommaNewline() { if (bRequiresComma) { Writer.WriteLine(","); } else if (Indent.Length > 0) { Writer.WriteLine(); } } void WriteValueInternal(string Name, string Value) { WriteCommaNewline(); Writer.Write("{0}\"{1}\": {2}", Indent, Name, Value); bRequiresComma = true; } void WriteEscapedString(string Value) { // Escape any characters which may not appear in a JSON string (see http://www.json.org). Writer.Write("\""); if (Value != null) { for (int Idx = 0; Idx < Value.Length; Idx++) { switch (Value[Idx]) { case '\"': Writer.Write("\\\""); break; case '\\': Writer.Write("\\\\"); break; case '\b': Writer.Write("\\b"); break; case '\f': Writer.Write("\\f"); break; case '\n': Writer.Write("\\n"); break; case '\r': Writer.Write("\\r"); break; case '\t': Writer.Write("\\t"); break; default: if (Char.IsControl(Value[Idx])) { Writer.Write("\\u{0:X4}", (int)Value[Idx]); } else { Writer.Write(Value[Idx]); } break; } } } Writer.Write("\""); } } }