UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

1026 lines
36 KiB

/**
* This class implements JSON object storage capabilities.
* It stores name-value pairs, where names are strings and values can be:
* ~ Boolean, string, null or number (float in this implementation) data;
* ~ Other JSON objects;
* ~ JSON Arrays (see `JArray` class).
*
* This implementation provides a variety of functionality,
* including parsing, displaying, getters and setters for JSON types that
* allow to freely set and fetch their values by name.
* JSON objects and arrays can be fetched by getters, but you cannot
* add existing object or array to another object. Instead one has to either
* clone existing object or create an empty one and then manually fill
* with data.
* This allows to avoid loop situations, where object is
* contained in itself.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class JObject extends JSON;
// We will store all our properties as a simple array of name-value pairs.
struct JProperty
{
var string name;
var JStorageAtom value;
};
// Bucket of alias-value pairs, with the same alias hash.
struct PropertyBucket
{
var array<JProperty> properties;
};
var private array<PropertyBucket> hashTable;
var private int storedElementCount;
// Reasonable lower and upper limits on hash table capacity,
// that will be enforced if user requires something outside those bounds
var private config const int MINIMUM_CAPACITY;
var private config const int MAXIMUM_CAPACITY;
// Minimum and maximum allowed density of elements
// (`storedElementCount / hashTable.length`).
// If density falls outside this range, - we have to resize hash table to
// get into (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds,
// as long as it does not violate other restrictions.
var private config const float MINIMUM_DENSITY;
var private config const float MAXIMUM_DENSITY;
// Only ever reallocate hash table if new size will differ by
// at least that much, regardless of other restrictions.
var private config const int MINIMUM_DIFFERENCE_FOR_REALLOCATION;
// Never use any hash table capacity below this limit,
// regardless of other variables
// (like `MINIMUM_CAPACITY` or `MINIMUM_DENSITY`).
var private config const int ABSOLUTE_LOWER_CAPACITY_LIMIT;
// Helper method that is needed as a replacement for `%`, since it is
// an operation on `float`s in UnrealScript and does not have enough precision
// to work with hashes.
// Assumes positive input.
private function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
// Finds indices for:
// 1. Bucked that contains specified alias (`bucketIndex`);
// 2. Pair for specified alias in the bucket's collection
// (`propertyIndex`).
// `bucketIndex` is always found,
// `propertyIndex` is valid iff method returns `true`, otherwise it's equal to
// the index at which new property can get inserted.
private final function bool FindPropertyIndices(
string name,
out int bucketIndex,
out int propertyIndex)
{
local int i;
local array<JProperty> bucketProperties;
TouchHashTable();
bucketIndex = _.text.GetHash(name);
if (bucketIndex < 0) {
bucketIndex *= -1;
}
bucketIndex = Remainder(bucketIndex, hashTable.length);
// Check if bucket actually has given name.
bucketProperties = hashTable[bucketIndex].properties;
for (i = 0; i < bucketProperties.length; i += 1)
{
if (bucketProperties[i].name == name)
{
propertyIndex = i;
return true;
}
}
propertyIndex = bucketProperties.length;
return false;
}
// Creates hash table in case it does not exist yet
private final function TouchHashTable()
{
if (hashTable.length <= 0) {
UpdateHashTableCapacity();
}
}
// Attempts to find a property in a caller `JObject` by the name `name` and
// writes it into `result`. Returns `true` if it succeeds and `false` otherwise
// (in that case writes a blank property with a given name in `result`).
private final function bool FindProperty(string name, out JProperty result)
{
local JProperty newProperty;
local int bucketIndex, propertyIndex;
if (FindPropertyIndices(name, bucketIndex, propertyIndex))
{
result = hashTable[bucketIndex].properties[propertyIndex];
return true;
}
newProperty.name = name;
result = newProperty;
return false;
}
// Creates/replaces a property with a name `newProperty.name` in caller
// JSON object
private final function UpdateProperty(JProperty newProperty)
{
local bool overriddenProperty;
local int bucketIndex, propertyIndex;
overriddenProperty = !FindPropertyIndices( newProperty.name,
bucketIndex, propertyIndex);
hashTable[bucketIndex].properties[propertyIndex] = newProperty;
if (overriddenProperty) {
storedElementCount += 1;
UpdateHashTableCapacity();
}
}
// Removes a property with a name `newProperty.name` from caller JSON object
// Returns `true` if something was actually removed.
private final function bool RemoveProperty(string propertyName)
{
local JProperty propertyToRemove;
local int bucketIndex, propertyIndex;
// Ensure has table was initialized before any updates
if (hashTable.length <= 0) {
UpdateHashTableCapacity();
}
if (FindPropertyIndices(propertyName, bucketIndex, propertyIndex)) {
propertyToRemove = hashTable[bucketIndex].properties[propertyIndex];
if (propertyToRemove.value.complexValue != none) {
propertyToRemove.value.complexValue.Destroy();
}
hashTable[bucketIndex].properties.Remove(propertyIndex, 1);
storedElementCount = Max(0, storedElementCount - 1);
UpdateHashTableCapacity();
return true;
}
return false;
}
// Checks if we need to change our current capacity and does so if needed
private final function UpdateHashTableCapacity()
{
local int oldCapacity, newCapacity;
oldCapacity = hashTable.length;
// Calculate new capacity (and whether it is needed) based on amount of
// stored properties and current capacity
newCapacity = oldCapacity;
if (storedElementCount < newCapacity * MINIMUM_DENSITY) {
newCapacity /= 2;
}
if (storedElementCount > newCapacity * MAXIMUM_DENSITY) {
newCapacity *= 2;
}
// Enforce our limits
newCapacity = Clamp(newCapacity, MINIMUM_CAPACITY, MAXIMUM_CAPACITY);
newCapacity = Max(ABSOLUTE_LOWER_CAPACITY_LIMIT, newCapacity);
// Only resize if difference is huge enough or table does not exists yet
if ( newCapacity - oldCapacity > MINIMUM_DIFFERENCE_FOR_REALLOCATION
|| oldCapacity - newCapacity > MINIMUM_DIFFERENCE_FOR_REALLOCATION
|| oldCapacity <= 0) {
ResizeHashTable(newCapacity);
}
}
// Change size of the hash table, does not check any limits, does not check
// if `newCapacity` is a valid capacity (`newCapacity > 0`).
// Use `UpdateHashTableCapacity()` for that.
private final function ResizeHashTable(int newCapacity)
{
local int i, j;
local array<JProperty> bucketProperties;
local array<PropertyBucket> oldHashTable;
oldHashTable = hashTable;
// Clean current hash table
hashTable.length = 0;
hashTable.length = newCapacity;
for (i = 0; i < oldHashTable.length; i += 1)
{
bucketProperties = oldHashTable[i].properties;
for (j = 0; j < bucketProperties.length; j += 1) {
UpdateProperty(bucketProperties[j]);
}
}
}
/**
* Returns `JType` of a property with a given name in our collection.
*
* This function can be used to check if certain variable exists
* in this object, since if such variable does not exist -
* function will return `JSON_Undefined`.
*
* @param name Name of the property to get the type of.
* @return Type of the property by the name `name`.
* `JSON_Undefined` iff property with that name does not exist.
*/
public final function JType GetTypeOf(string name)
{
local JProperty property;
FindProperty(name, property);
// If we did not find anything - `property` will be set up as
// a `JSON_Undefined` type value.
return property.value.type;
}
/**
* Gets the value (as a `float`) of a property by the name `name`,
* assuming it has `JSON_Number` type.
*
* Forms a pair with `GetInteger()` method. JSON allows to specify
* arbitrary precision for the number variables, but UnrealScript can only
* store a limited range of numeric value.
* To alleviate this problem we store numeric JSON values as both
* `float` and `int` and can return either of the requested versions.
*
* @param name Name of the property to return a value of.
* @param defaultValue Value to return if property does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return Number value of the property under name `name`,
* if it exists and has `JSON_Number` type.
* Otherwise returns passed `defaultValue`.
*/
public final function float GetNumber(string name, optional float defaultValue)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Number) {
return defaultValue;
}
return property.value.numberValue;
}
/**
* Gets the value (as an `int`) of a property by the name `name`,
* assuming it has `JSON_Number` type.
*
* Forms a pair with `GetNumber()` method. JSON allows to specify
* arbitrary precision for the number variables, but UnrealScript can only
* store a limited range of numeric value.
* To alleviate this problem we store numeric JSON values as both
* `float` and `int` and can return either of the requested versions.
*
* @param name Name of the property to return a value of.
* @param defaultValue Value to return if property does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return Number value of the property under name `name`,
* if it exists and has `JSON_Number` type.
* Otherwise returns passed `defaultValue`.
*/
public final function int GetInteger(string name, optional int defaultValue)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Number) {
return defaultValue;
}
return property.value.numberValueAsInt;
}
/**
* Gets the value of a property by the name `name`,
* assuming it has `JSON_String` type.
*
* See also `GetClass()` method.
*
* @param name Name of the property to return a value of.
* @param defaultValue Value to return if property does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return String value of the property under name `name`,
* if it exists and has `JSON_String` type.
* Otherwise returns passed `defaultValue`.
*/
public final function string GetString(
string name,
optional string defaultValue
)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_String) {
return defaultValue;
}
return property.value.stringValue;
}
/**
* Gets the value of a property by the name `name` as a `class`,
* assuming it has `JSON_String` type.
*
* JSON does not support to store class data type, but we can use string type
* for that. This method attempts to load a class object from it's full name,
* (like `Engine.Actor`) recorded inside an appropriate string value.
*
* @param name Name of the property to return a value of.
* @param defaultValue Value to return if property does not exist,
* has a different type (can be checked by `GetTypeOf()`) or not
* a valid class name.
* @return Class value of the property under name `name`,
* if it exists, has `JSON_String` type and it represents
* a full name of some class.
* Otherwise returns passed `defaultValue`.
*/
public final function class<Object> GetClass(
string name,
optional class<Object> defaultValue
)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_String) {
return defaultValue;
}
TryLoadingStringAsClass(property.value);
if (property.value.stringValueAsClass != none) {
return property.value.stringValueAsClass;
}
return defaultValue;
}
/**
* Gets the value of a property by the name `name`,
* assuming it has `JSON_Boolean` type.
*
* @param name Name of the property to return a value of.
* @param defaultValue Value to return if property does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return Boolean value of the property under name `name`,
* if it exists and has `JSON_Boolean` type.
* Otherwise returns passed `defaultValue`.
*/
public final function bool GetBoolean(string name, optional bool defaultValue)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Boolean) {
return defaultValue;
}
return property.value.booleanValue;
}
/**
* Checks if a property by the name `name` has `JSON_Null` type.
*
* Alternatively consider using `GetType()` method.
*
* @param name Name of the property to check for being `null`.
* @return `true` if property under given name `name` exists and
* has type `JSON_Null`; `false` otherwise.
*/
public final function bool IsNull(string name)
{
local JProperty property;
FindProperty(name, property);
return (property.value.type == JSON_Null);
}
/**
* Gets the value of a property by the name `name`,
* assuming it has `JSON_Array` type.
*
* @param name Name of the property to return a value of.
* @return `JArray` object value of the property under name `name`,
* if it exists and has `JSON_Array` type.
* Otherwise returns `none`.
*/
public final function JArray GetArray(string name)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Array) {
return none;
}
return JArray(property.value.complexValue);
}
/**
* Gets the value of a property by the name `name`,
* assuming it has `JSON_Object` type.
*
* @param name Name of the property to return a value of.
* @return `JObject` object value of the property under name `name`,
* if it exists and has `JSON_Object` type.
* Otherwise returns `none`.
*/
public final function JObject GetObject(string name)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Object) return none;
return JObject(property.value.complexValue);
}
/**
* Sets the number value (as `float`) of a property by the name `name`,
* erasing previous value (if it was recorded).
*
* Property in question will have `JSON_Number` type.
*
* Forms a pair with `SetInteger()` method.
* While JSON standard allows to store numbers with arbitrary precision,
* UnrealScript's types have a limited range.
* To alleviate this problem we store numbers in both `float`- and
* `int`-type variables to extended supported range of values.
* So if you need to store a number with fractional part, you should
* prefer `SetNumber()` and for integer values `SetInteger()` is preferable.
* Both will create a property of type `JSON_Number`.
*
* @param name Name of the property to set a value of.
* @param value Value to set to a property under a given name `name`.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetNumber(string name, float value)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Number;
property.value.numberValue = value;
property.value.numberValueAsInt = int(value);
property.value.complexValue = none;
property.value.preferIntegerValue = false;
UpdateProperty(property);
return self;
}
/**
* Sets the number value (as `int`) of a property by the name `name`,
* erasing previous value (if it was recorded).
*
* Property in question will have `JSON_Number` type.
*
* Forms a pair with `SetNumber()` method.
* While JSON standard allows to store numbers with arbitrary precision,
* UnrealScript's types have a limited range.
* To alleviate this problem we store numbers in both `float`- and
* `int`-type variables to extended supported range of values.
* So if you need to store a number with fractional part, you should
* prefer `SetNumber()` and for integer values `SetInteger()` is preferable.
* Both will create a property of type `JSON_Number`.
*
* @param name Name of the property to set a value of.
* @param value Value to set to a property under a given name `name`.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetInteger(string name, int value)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Number;
property.value.numberValue = float(value);
property.value.numberValueAsInt = value;
property.value.complexValue = none;
property.value.preferIntegerValue = true;
UpdateProperty(property);
return self;
}
/**
* Sets the string value of a property by the name `name`,
* erasing previous value (if it was recorded).
*
* Property in question will have `JSON_String` type.
*
* Also see `SetClass()` method.
*
* @param name Name of the property to set a value of.
* @param value Value to set to a property under a given name `name`.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetString(string name, string value)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_String;
property.value.stringValue = value;
property.value.stringValueAsClass = none;
property.value.classLoadingWasAttempted = false;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
/**
* Sets the string value, corresponding to a given class `value`,
* of a property by the name `name`, erasing previous value
* (if it was recorded).
*
* Property in question will have `JSON_String` type.
*
* We want to allow storing `class` data in our JSON containers, but JSON
* standard does not support such a type, so we have to use string type
* to store `class`' name instead.
* Also see `GetClass()` method`.
*
* @param name Name of the property to set a value of.
* @param value Value to set to a property under a given name `name`.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetClass(string name, class<Object> value)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_String;
property.value.stringValue = string(value);
property.value.stringValueAsClass = value;
property.value.classLoadingWasAttempted = true;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
/**
* Sets the boolean value of a property by the name `name`,
* erasing previous value (if it was recorded).
*
* Property in question will have `JSON_Boolean` type.
*
* @param name Name of the property to set a value of.
* @param value Value to set to a property under a given name `name`.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetBoolean(string name, bool value)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Boolean;
property.value.booleanValue = value;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
/**
* Sets the value of a property by the name `name` to be "null" (`JSON_Null`),
* erasing previous value (if it was recorded).
*
* Property in question will have `JSON_Null` type.
*
* @param name Name of the property to set a "null" value to.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetNull(string name)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Null;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
/**
* Sets the value of a property by the name `name` to store `JArray` object
* (JSON array type).
*
* NOTE: This method DOES NOT make caller `JObject` store a
* given reference, instead it clones it (see `Clone()`) into a new copy and
* stores that. This is made this way to ensure you can not, say, store
* an object in itself or it's children.
* See also `CreateArray()` method.
*
* @param name Name of the property to return a value of.
* @param template Template `JArray` to clone into property.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetArray(string name, JArray template)
{
local JProperty property;
if (template == none) return self;
FindProperty(name, property);
if (property.value.complexValue != none) {
property.value.complexValue.Destroy();
}
property.value.type = JSON_Array;
property.value.complexValue = template.Clone();
UpdateProperty(property);
return self;
}
/**
* Sets the value of a property by the name `name` to store `JObject` object
* (JSON object type).
*
* NOTE: This method DOES NOT make caller `JObject` store a
* given reference, instead it clones it (see `Clone()`) into a new copy and
* stores that. This is made this way to ensure you can not, say, store
* an object in itself or it's children.
* See also `CreateArray()` method.
*
* @param name Name of the property to return a value of.
* @param template Template `JObject` to clone into property.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject SetObject(string name, JObject template)
{
local JProperty property;
if (template == none) return self;
FindProperty(name, property);
if (property.value.complexValue != none) {
property.value.complexValue.Destroy();
}
property.value.type = JSON_Object;
property.value.complexValue = template.Clone();
UpdateProperty(property);
return self;
}
/**
* Sets the value of a property by the name `name` to store a new
* `JArray` object (JSON array type).
*
* See also `SetArray()` method.
*
* @param name Name of the property to store the new `JArray` value.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject CreateArray(string name)
{
local JProperty property;
FindProperty(name, property);
if (property.value.complexValue != none) {
property.value.complexValue.Destroy();
}
property.value.type = JSON_Array;
property.value.complexValue = _.json.NewArray();
UpdateProperty(property);
return self;
}
/**
* Sets the value of a property by the name `name` to store a new
* `JObject` object (JSON array type).
*
* See also `SetArray()` method.
*
* @param name Name of the property to store the new `JObject` value.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject CreateObject(string name)
{
local JProperty property;
FindProperty(name, property);
if (property.value.complexValue != none) {
property.value.complexValue.Destroy();
}
property.value.type = JSON_Object;
property.value.complexValue = _.json.NewObject();
UpdateProperty(property);
return self;
}
/**
* Removes a property with a given name.
*
* Does nothing if property with a given name does not exist.
*
* @param name Name of the property to remove.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JObject RemoveValue(string name)
{
RemoveProperty(name);
return self;
}
/**
* Completely clears caller `JObject` of all stored properties.
*/
public function Clear()
{
local int i, j;
local array<JProperty> nextProperties;
for (i = 0; i < hashTable.length; i += 1)
{
nextProperties = hashTable[i].properties;
for (j = 0; j < nextProperties.length; j += 1)
{
if (nextProperties[j].value.complexValue == none) continue;
nextProperties[j].value.complexValue.Destroy();
}
}
}
/**
* Returns names of all properties inside caller `JObject`.
*
* @return Array of all the caller object's property names as `string`s.
*/
public final function array<string> GetPropertyNames()
{
local int i, j;
local array<string> result;
local array<JProperty> nextProperties;
for (i = 0; i < hashTable.length; i += 1)
{
nextProperties = hashTable[i].properties;
for (j = 0; j < nextProperties.length; j += 1) {
result[result.length] = nextProperties[j].name;
}
}
return result;
}
/**
* Checks if caller JSON container's values form a subset of
* `rightJSON`'s values.
*
* @return `true` if caller ("left") object is a subset of `rightJSON`
* and `false` otherwise.
*/
public function bool IsSubsetOf(JSON rightJSON)
{
local int i, j;
local JObject rightObject;
local JProperty rightProperty;
local array<JProperty> nextProperties;
rightObject = JObject(rightJSON);
if (rightObject == none) return false;
for (i = 0; i < hashTable.length; i += 1)
{
nextProperties = hashTable[i].properties;
for (j = 0; j < nextProperties.length; j += 1) {
rightObject.FindProperty(nextProperties[j].name, rightProperty);
if (rightProperty.value.type == JSON_Undefined) {
return false;
}
if (!AreAtomsEqual(nextProperties[j].value, rightProperty.value)) {
return false;
}
}
}
return true;
}
/**
* Makes an exact copy of the caller `JObject`
*
* @return Copy of the caller `JObject`. Guaranteed to be `JObject`
* (or `none`, if appropriate object could not be created).
*/
public function JSON Clone()
{
local int i, j;
local JObject clonedObject;
local array<PropertyBucket> clonedHashTable;
local array<JProperty> nextProperties;
clonedObject = _.json.NewObject();
if (clonedObject == none)
{
_.logger.Failure("Cannot clone `JObject`: cannot spawn a"
@ "new instance.");
return none;
}
clonedHashTable = hashTable;
for (i = 0; i < clonedHashTable.length; i += 1)
{
nextProperties = clonedHashTable[i].properties;
for (j = 0; j < nextProperties.length; j += 1)
{
if (nextProperties[j].value.complexValue == none) continue;
if ( nextProperties[j].value.type != JSON_Array
&& nextProperties[j].value.type != JSON_Object) {
continue;
}
nextProperties[j].value.complexValue =
nextProperties[j].value.complexValue.Clone();
}
clonedHashTable[i].properties = nextProperties;
}
clonedObject.hashTable = clonedHashTable;
return clonedObject;
}
/**
* Uses given parser to parse a new set of properties inside
* the caller `JObject`.
*
* Only adds new properties if parsing the whole object was successful,
* otherwise even successfully parsed properties will be discarded.
*
* `parser` must point at the text describing a JSON object in
* a valid notation. Then it parses that container inside memory, but
* instead of creating it as a separate entity, adds it's values to
* the caller container.
* Everything that comes after parsed `JObject` is discarded.
*
* This method does not try to validate passed JSON and can accept invalid
* JSON by making some assumptions, but it is an undefined behavior and
* one should not expect it.
* Method is only guaranteed to work on valid JSON.
*
* @param parser Parser that method would use to parse `JObject` from
* wherever it left. It's confirmed will not be changed, but if parsing
* was successful, - it will point at the next available character.
* Do not treat `parser` being in a non-failed state as a confirmation of
* successful parsing: JSON parsing might fail regardless.
* Check return value for that.
* @return `true` if parsing was successful and `false` otherwise.
*/
public function bool ParseIntoSelfWith(Parser parser)
{
local bool parsingSucceeded;
local Parser.ParserState initState, confirmedState;
local JProperty nextProperty;
local array<JProperty> parsedProperties;
if (parser == none) return false;
initState = parser.GetCurrentState();
// Ensure that parser starts pointing at what looks like a JSON object
confirmedState = parser.Skip().Match("{").GetCurrentState();
if (!parser.Ok())
{
parser.RestoreState(initState);
return false;
}
while (parser.Ok() && !parser.HasFinished())
{
confirmedState = parser.Skip().GetCurrentState();
// Check for JSON object ending and ONLY THEN declare parsing
// is successful, not encountering '}' implies bad JSON format.
if (parser.Match("}").Ok())
{
parsingSucceeded = true;
break;
}
if ( parsedProperties.length > 0
&& !parser.RestoreState(confirmedState).Match(",").Skip().Ok()) {
break;
}
// Recover after failed `Match("}")` on the first cycle
// (`parsedProperties.length == 0`)
else if (parser.Ok()) {
confirmedState = parser.GetCurrentState();
}
// Parse property
parser.RestoreState(confirmedState).Skip();
parser.MStringLiteral(nextProperty.name).Skip().Match(":");
nextProperty.value = ParseAtom(parser.Skip());
if (!parser.Ok() || nextProperty.value.type == JSON_Undefined) {
break;
}
parsedProperties[parsedProperties.length] = nextProperty;
}
HandleParsedProperties(parsedProperties, parsingSucceeded);
if (!parsingSucceeded) {
parser.RestoreState(initState);
}
return parsingSucceeded;
}
// Either cleans up or adds a list of parsed properties,
// depending on whether parsing was successful or not.
private function HandleParsedProperties(
array<JProperty> parsedProperties,
bool parsingSucceeded)
{
local int i;
if (parsingSucceeded)
{
for (i = 0; i < parsedProperties.length; i += 1) {
UpdateProperty(parsedProperties[i]);
}
return;
}
for (i = 0; i < parsedProperties.length; i += 1)
{
if (parsedProperties[i].value.complexValue != none) {
parsedProperties[i].value.complexValue.Destroy();
}
}
}
/**
* Displays caller `JObject` with a provided preset.
*
* See `Display()` for a simpler to use method.
*
* @param displaySettings Struct that describes precisely how to display
* caller `JObject`. Can be used to emulate `Display()` call.
* @return String representation of caller JSON container in format defined by
* `displaySettings`.
*/
public function string DisplayWith(JSONDisplaySettings displaySettings)
{
local int i, j;
local bool isntFirstProperty;
local string contents;
local string openingBraces, closingBraces;
local string propertiesSeparator;
local array<JProperty> nextProperties;
local JSONDisplaySettings innerSettings;
if (displaySettings.stackIndentation) {
innerSettings = IndentSettings(displaySettings);
}
else {
innerSettings = displaySettings;
}
GetBraces(openingBraces, closingBraces, displaySettings, innerSettings);
propertiesSeparator = "," $ innerSettings.afterObjectComma;
if (innerSettings.colored) {
propertiesSeparator = "{$json_comma" @ propertiesSeparator $ "}";
openingBraces = "{$json_objectBraces &" $ openingBraces $ "}";
closingBraces = "{$json_objectBraces &" $ closingBraces $ "}";
}
// Display inner properties
for (i = 0; i < hashTable.length; i += 1)
{
nextProperties = hashTable[i].properties;
for (j = 0; j < nextProperties.length; j += 1)
{
if (isntFirstProperty) {
contents $= propertiesSeparator;
}
contents $= DisplayProperty(nextProperties[j], innerSettings);
isntFirstProperty = true;
}
}
return openingBraces $ contents $ closingBraces;
}
/**
* Helper function that generates `string`s to be used for opening and
* closing braces for text representation of the caller `JObject`.
*
* Cannot fail.
*
* @param openingBraces `string` for opening braces will be recorded here.
* @param closingBraces `string` for closing braces will be recorded here.
* @param outerSettings Settings that were passed to tell us how to display
* a caller object.
* @param innerSettings Settings that were generated from `outerSettings` by
* indenting them (`IndentSettings()`) to use to display it's
* inner properties.
*/
protected function GetBraces(
out string openingBraces,
out string closingBraces,
JSONDisplaySettings outerSettings,
JSONDisplaySettings innerSettings)
{
openingBraces = "{";
closingBraces = "}";
if (innerSettings.colored) {
openingBraces = "&" $ openingBraces;
closingBraces = "&" $ closingBraces;
}
// We only use inner settings for the part right after '{',
// as the rest is supposed to be aligned with outer objects
openingBraces = outerSettings.beforeObjectOpening
$ openingBraces $ innerSettings.afterObjectOpening;
closingBraces = outerSettings.beforeObjectEnding
$ closingBraces $ outerSettings.afterObjectEnding;
}
/**
* Helper method to convert a JSON object's property into it's
* text representation.
*
* @param toDisplay Property to display as a `string`.
* @param displaySettings Settings that tells us how to display it.
* @return `string` representation of a given property `toDisplay`,
* created according to the settings `displaySettings`.
*/
protected function string DisplayProperty(
JProperty toDisplay,
JSONDisplaySettings displaySettings)
{
local string result;
result = displaySettings.beforePropertyName
$ DisplayJSONString(toDisplay.name)
$ displaySettings.afterPropertyName;
if (displaySettings.colored) {
result = "{$json_propertyName" @ result $ "}{$json_colon :}";
}
else {
result $= ":";
}
return (result $ displaySettings.beforePropertyValue
$ DisplayAtom(toDisplay.value, displaySettings)
$ displaySettings.afterPropertyValue);
}
defaultproperties
{
ABSOLUTE_LOWER_CAPACITY_LIMIT = 10
MINIMUM_CAPACITY = 50
MAXIMUM_CAPACITY = 100000
MINIMUM_DENSITY = 0.25
MAXIMUM_DENSITY = 0.75
MINIMUM_DIFFERENCE_FOR_REALLOCATION = 50
}