// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Collections.Specialized;
using System.Collections;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
namespace EpicGames.Core
{
using SystemJsonObject = System.Text.Json.Nodes.JsonObject;
using SystemJsonNode = System.Text.Json.Nodes.JsonNode;
using SystemJsonValue = System.Text.Json.Nodes.JsonValue;
using SystemJsonArray = System.Text.Json.Nodes.JsonArray;
///
/// Extension methods for OrderedDictionary to have functionality more similar to Dictionary.
///
public static class OrderedDictionaryExtensions
{
///
/// Tries to get a value associated with a key in the OrderedDictionary.
///
///
/// The key to find a value for.
/// The value associated with the key. Is null if no key is found.
/// Returns true if the associated value with the key exists and is found. Else returns false.
public static bool TryGetValue(this OrderedDictionary dictionary, object key, [NotNullWhen(true)] out object? value)
{
if (dictionary.Contains(key))
{
value = dictionary[key]!;
return true;
}
value = null;
return false;
}
private static object? DeepCopyHelper(object? original)
{
if (original is null)
{
return null;
}
if (original is OrderedDictionary dictionary)
{
OrderedDictionary copy = new OrderedDictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (DictionaryEntry entry in dictionary)
{
copy.Add(entry.Key, DeepCopyHelper(entry.Value));
}
return copy;
}
if (original is Array array)
{
Array copy = Array.CreateInstance(array.GetType().GetElementType()!, array.Length);
for (int index = 0; index < array.Length; ++index)
{
copy.SetValue(DeepCopyHelper(array.GetValue(index)), index);
}
return copy;
}
// It's a value type or a string, it's safe to just copy it
return original;
}
///
/// Creates a deep copy of all the elements in an OrderedDictionary.
///
///
/// An ordered dictionary with a deep copy of elements from dictionary.
public static OrderedDictionary CreateDeepCopy(this OrderedDictionary dictionary)
{
OrderedDictionary copy = new OrderedDictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (DictionaryEntry entry in dictionary)
{
copy.Add(entry.Key, DeepCopyHelper(entry.Value));
}
return copy;
}
}
///
/// Stores a JSON object in memory
///
public class JsonObject
{
readonly OrderedDictionary _rawOrderedObject;
///
/// Default constructor. Use this to create new JsonObject elements and use the AddOrSetFieldValue API to add values to this JsonObject.
///
public JsonObject()
{
_rawOrderedObject = new OrderedDictionary(StringComparer.InvariantCultureIgnoreCase);
}
///
/// Construct a JSON object from the OrderedDictionary obtained from reading a file on disk or parsing valid json text.
///
/// Raw object parsed from disk
private JsonObject(OrderedDictionary inRawObject)
{
_rawOrderedObject = inRawObject.CreateDeepCopy();
}
///
/// Constructor
///
///
public JsonObject(JsonElement element)
{
_rawOrderedObject = new OrderedDictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (JsonProperty property in element.EnumerateObject())
{
_rawOrderedObject[property.Name] = ParseElement(property.Value);
}
}
///
/// Override of the Equals method to check for equality between 2 JsonObject instances.
///
///
/// Returns true if this object and obj are equal. Else returns false.
public override bool Equals(object? obj)
{
if (obj is null || GetType() != obj.GetType())
{
return false;
}
JsonObject jsonObject = (JsonObject) obj;
if (_rawOrderedObject.Count != jsonObject._rawOrderedObject.Count)
{
return false;
}
foreach (DictionaryEntry entry in _rawOrderedObject)
{
if (!jsonObject._rawOrderedObject.Contains(entry.Key))
{
return false;
}
if (!entry.Value!.Equals(jsonObject._rawOrderedObject[entry.Key!]))
{
return false;
}
}
return true;
}
///
/// Returns the calculated hash code for a JsonObject instance.
///
/// Returns the hash code for a JsonObject instance.
public override int GetHashCode()
{
return HashCode.Combine(_rawOrderedObject);
}
///
/// Parse an individual element
///
///
///
public static object? ParseElement(JsonElement element)
{
switch(element.ValueKind)
{
case JsonValueKind.Array:
return element.EnumerateArray().Select(x => ParseElement(x)).ToArray();
case JsonValueKind.Number:
return element.GetDouble();
case JsonValueKind.Object:
OrderedDictionary dictionary = new OrderedDictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (JsonProperty property in element.EnumerateObject())
{
dictionary.Add(property.Name, ParseElement(property.Value));
}
return dictionary;
case JsonValueKind.String:
return element.GetString();
case JsonValueKind.False:
return false;
case JsonValueKind.True:
return true;
case JsonValueKind.Null:
return null;
default:
throw new NotImplementedException();
}
}
private static SystemJsonNode ToJsonNode(object? obj)
{
// All values in the JsonObject are either parsed from a string, read from a file or set/added with the API
// All values at this point must be supported and the correct types
switch (obj)
{
case OrderedDictionary objDictionary:
SystemJsonObject dictionaryJson= [];
foreach (object? key in objDictionary.Keys)
{
dictionaryJson.Add((string) key, ToJsonNode(objDictionary[key]));
}
return dictionaryJson;
case Array objArray:
SystemJsonArray tempJsonArray = [];
foreach (object? element in objArray)
{
tempJsonArray.Add(ToJsonNode(element));
}
return tempJsonArray;
default:
// We support null as per the json spec
return SystemJsonValue.Create(obj)!;
}
}
///
/// Converts a JsonObject instance to a System.Text.Json.Nodes.JsonObject instance. Users can then use .NET facilities like Utf8JsonWriter to interact with this JsonObject.
///
/// Returns the System.Text.Json.Nodes.JsonObject representation of this object.
public SystemJsonObject ToSystemJsonObject()
{
SystemJsonObject returnObject = [];
foreach (DictionaryEntry entry in _rawOrderedObject )
{
returnObject.Add((string) entry.Key, ToJsonNode(entry.Value));
}
return returnObject;
}
///
/// 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 (JsonException)
{
throw;
}
catch (Exception ex)
{
throw new JsonException($"Unable to parse {file}: {ex.Message}", file.FullName, null, null, ex );
}
}
///
/// 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, [NotNullWhen(true)] 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)
{
try
{
JsonDocument document = JsonDocument.Parse(text, new JsonDocumentOptions { AllowTrailingCommas = true });
return new JsonObject(document.RootElement);
}
catch (JsonException)
{
throw;
}
catch (Exception ex)
{
throw new JsonException($"Failed to parse json text '{text}'. {ex.Message}", ex);
}
}
///
/// 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, [NotNullWhen(true)] 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 => _rawOrderedObject.Keys.Cast();
///
/// 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 JsonException($"Missing or invalid '{fieldName}' field");
}
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, [NotNullWhen(true)] out string? result)
{
object? rawValue;
if (_rawOrderedObject.TryGetValue(fieldName, out rawValue) && (rawValue is string strValue))
{
result = strValue;
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 JsonException($"Missing or invalid '{fieldName}' field");
}
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, [NotNullWhen(true)] out string[]? result)
{
object? rawValue;
if (_rawOrderedObject.TryGetValue(fieldName, out rawValue) && (rawValue is IEnumerable