/**
* API that provides methods for creating/destroying and managing available
* databases.
* Copyright 2021-2023 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 .
*/
class DBAPI extends AcediaObject;
var private const class localDBClass;
// Store all already loaded databases to make sure we do not create two
// different `LocalDatabaseInstance` that are trying to make changes
// separately.
var private HashTable loadedLocalDatabases;
var private LoggerAPI.Definition infoLocalDatabaseCreated;
var private LoggerAPI.Definition infoLocalDatabaseDeleted;
var private LoggerAPI.Definition infoLocalDatabaseLoaded;
private final function CreateLocalDBMapIfMissing()
{
if (loadedLocalDatabases == none) {
loadedLocalDatabases = __().collections.EmptyHashTable();
}
}
/**
* Loads database based on the link.
*
* Links have the form of ":" (or, optionally, "[]:"),
* followed by the JSON pointer (possibly empty one) to the object inside it.
* "" can be either "local" or "remote" and is necessary only when both
* local and remote database have the same name (which should be avoided).
* "" refers to the database that we are expected
* to load, it has to consist of numbers and latin letters only.
*
* @param databaseLink Link from which to extract database's name.
* @return Database named "" of type "" from the `databaseLink`.
*/
public final function Database Load(BaseText databaseLink)
{
local Parser parser;
local Database result;
local MutableText databaseName;
if (databaseLink == none) {
return none;
}
parser = _.text.Parse(databaseLink);
// Only local DBs are supported for now!
// So just consume this prefix, if it's present.
parser.Match(P("[local]")).Confirm();
parser.R().MUntil(databaseName, _.text.GetCharacter(":")).MatchS(":");
if (!parser.Ok())
{
parser.FreeSelf();
return none;
}
result = LoadLocal(databaseName);
parser.FreeSelf();
databaseName.FreeSelf();
return result;
}
/**
* Extracts `MutableJSONPointer` from the database path, given by `databaseLink`.
*
* Links have the form of ":" (or, optionally, "[]:"),
* followed by the JSON pointer (possibly empty one) to the object inside it.
* "" can be either "local" or "remote" and is necessary only when both
* local and remote database have the same name (which should be avoided).
* "" refers to the database that we are expected
* to load, it has to consist of numbers and latin letters only.
* This method returns `MutableJSONPointer` that comes after type-name pair.
*
* @param Link from which to extract `MutableJSONPointer`.
* @return `MutableJSONPointer` from the database link.
* Guaranteed to not be `none` if provided argument `databaseLink`
* is not `none`.
*/
public final function MutableJsonPointer GetMutablePointer(BaseText databaseLink)
{
local int slashIndex;
local Text textPointer;
local MutableJsonPointer result;
if (databaseLink == none) {
return none;
}
slashIndex = databaseLink.IndexOf(P(":"));
if (slashIndex < 0) {
return MutableJsonPointer(_.memory.Allocate(class'MutableJsonPointer'));
}
textPointer = databaseLink.Copy(slashIndex + 1);
result = _.json.MutablePointer(textPointer);
textPointer.FreeSelf();
return result;
}
/**
* Extracts `JSONPointer` from the database path, given by `databaseLink`.
*
* Links have the form of ":" (or, optionally, "[]:"),
* followed by the JSON pointer (possibly empty one) to the object inside it.
* "" can be either "local" or "remote" and is necessary only when both
* local and remote database have the same name (which should be avoided).
* "" refers to the database that we are expected
* to load, it has to consist of numbers and latin letters only.
* This method returns `JSONPointer` that comes after type-name pair.
*
* @param Link from which to extract `JSONPointer`.
* @return `JSONPointer` from the database link.
* Guaranteed to not be `none` if provided argument `databaseLink`
* is not `none`.
*/
public final function JSONPointer GetPointer(BaseText databaseLink)
{
local int slashIndex;
local Text textPointer;
local JSONPointer result;
if (databaseLink == none) {
return none;
}
slashIndex = databaseLink.IndexOf(P(":"));
if (slashIndex < 0) {
return JSONPointer(_.memory.Allocate(class'JSONPointer'));
}
textPointer = databaseLink.Copy(slashIndex + 1);
result = _.json.Pointer(textPointer);
textPointer.FreeSelf();
return result;
}
/**
* Opens a new `DBConnection` to the data referred to by the database link.
*
* Opened `DBConnection` doesn't automatically start a connection, so you
* need to call its `Connect()` method.
*
* @param databaseLink Database link to the data we want to connect to.
* @return Initialized `DBConnection` in case given link is valid and `none`
* otherwise.
*/
public final function DBConnection OpenConnection(BaseText databaseLink)
{
local DBConnection result;
local Parser parser;
local Database databaseToConnect;
local JSONPointer locationToConnect;
local MutableText databaseName, textPointer;
if (databaseLink == none) {
return none;
}
parser = _.text.Parse(databaseLink);
// Only local DBs are supported for now!
// So just consume this prefix, if it's present.
parser.Match(P("[local]")).Confirm();
textPointer = parser
.R()
.MUntil(databaseName, _.text.GetCharacter(":"))
.MatchS(":")
.GetRemainderM();
if (parser.Ok())
{
databaseToConnect = LoadLocal(databaseName);
locationToConnect = _.json.Pointer(textPointer);
result = DBConnection(_.memory.Allocate(class'DBConnection'));
result.Initialize(databaseToConnect, locationToConnect);
_.memory.Free(databaseToConnect);
_.memory.Free(locationToConnect);
}
parser.FreeSelf();
_.memory.Free(databaseName);
_.memory.Free(textPointer);
if (result != none && !result.IsInitialized())
{
result.FreeSelf();
result = none;
}
return result;
}
/**
* Creates new local database with name `databaseName`.
*
* This method will fail if:
* 1. `databaseName` is `none` or empty;
* 2. Local database with name `databaseName` already exists.
*
* @param databaseName Name for the new database.
* @return Reference to created database. Returns `none` iff method failed.
*/
public final function LocalDatabaseInstance NewLocal(BaseText databaseName)
{
local DBRecord rootRecord;
local Text rootRecordName;
local Text databaseNameCopy;
local LocalDatabase newConfig;
local LocalDatabaseInstance newLocalDBInstance;
CreateLocalDBMapIfMissing();
if (databaseName == none) return none;
if (!databaseName.IsValidName()) return none;
newConfig = class'LocalDatabase'.static.Load(databaseName);
if (newConfig == none) return none;
if (newConfig.HasDefinedRoot()) return none;
if (loadedLocalDatabases.HasKey(databaseName)) return none;
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
databaseNameCopy = databaseName.Copy();
loadedLocalDatabases.SetItem(databaseNameCopy, newLocalDBInstance);
rootRecord = class'DBRecord'.static.NewRecord(databaseName);
rootRecordName = _.text.FromString(string(rootRecord.name));
newConfig.SetRootName(rootRecordName);
newConfig.Save();
newLocalDBInstance.Initialize(newConfig, rootRecord);
_.logger.Auto(infoLocalDatabaseCreated).Arg(databaseNameCopy);
_.memory.Free(rootRecordName);
return newLocalDBInstance;
}
/**
* Loads and returns local database with the name `databaseName`.
*
* If specified database is already loaded - simply returns it's reference
* (consequent calls to `LoadLocal()` will keep returning the same reference,
* unless database is deleted).
*
* @param databaseName Name of the database to load.
* @return Loaded local database. `none` if it does not exist.
*/
public final function LocalDatabaseInstance LoadLocal(BaseText databaseName)
{
local DBRecord rootRecord;
local Text rootRecordName;
local LocalDatabase newConfig;
local LocalDatabaseInstance newLocalDBInstance, result;
local Text dbKey;
if (databaseName == none) {
return none;
}
CreateLocalDBMapIfMissing();
dbKey = databaseName.Copy();
if (loadedLocalDatabases.HasKey(dbKey))
{
result = LocalDatabaseInstance(loadedLocalDatabases.GetItem(dbKey));
_.memory.Free(dbKey);
return result;
}
// No need to check `dbKey` for being valid,
// since `Load()` will just return `none` if it is not.
newConfig = class'LocalDatabase'.static.Load(dbKey);
if (newConfig == none) {
_.memory.Free(dbKey);
return none;
}
if (!newConfig.HasDefinedRoot() && !newConfig.ShouldCreateIfMissing()) {
_.memory.Free(dbKey);
return none;
}
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance);
dbKey.FreeSelf();
if (newConfig.HasDefinedRoot())
{
rootRecordName = newConfig.GetRootName();
rootRecord = class'DBRecord'.static.LoadRecord(rootRecordName, dbKey);
}
else
{
rootRecord = class'DBRecord'.static.NewRecord(dbKey);
rootRecordName = _.text.FromString(string(rootRecord.name));
newConfig.SetRootName(rootRecordName);
newConfig.Save();
}
newLocalDBInstance.Initialize(newConfig, rootRecord);
_.logger.Auto(infoLocalDatabaseLoaded).Arg(dbKey);
_.memory.Free(rootRecordName);
return newLocalDBInstance;
}
/**
* Checks if local database with the name `databaseName` already exists.
*
* @param databaseName Name of the database to check.
* @return `true` if database with specified name exists and `false` otherwise.
*/
public final function bool ExistsLocal(BaseText databaseName)
{
local bool result;
local LocalDatabaseInstance instance;
instance = LoadLocal(databaseName);
result = (instance != none);
_.memory.Free(instance);
return result;
}
/**
* Deletes local database with name `databaseName`.
*
* @param databaseName Name of the database to delete.
* @return `true` if database with specified name existed and was deleted and
* `false` otherwise.
*/
public final function bool DeleteLocal(BaseText databaseName)
{
local LocalDatabase localDatabaseConfig;
local LocalDatabaseInstance localDatabase;
local HashTable.Entry dbEntry;
if (databaseName == none) {
return false;
}
CreateLocalDBMapIfMissing();
// To delete database we first need to load it
localDatabase = LoadLocal(databaseName);
if (localDatabase != none)
{
localDatabaseConfig = localDatabase.GetConfig();
localDatabase.WriteToDisk();
_.memory.Free(localDatabase);
}
dbEntry = loadedLocalDatabases.TakeEntry(databaseName);
// Delete `LocalDatabaseInstance` before erasing the package,
// to allow it to clean up safely
_.memory.Free(dbEntry.key);
_.memory.Free(dbEntry.value);
if (localDatabaseConfig != none)
{
EraseAllPackageData(localDatabaseConfig.GetPackageName());
localDatabaseConfig.DeleteSelf();
_.logger.Auto(infoLocalDatabaseDeleted).Arg(databaseName.Copy());
return true;
}
return false;
}
private function EraseAllPackageData(BaseText packageToErase)
{
local int i;
local string packageName;
local GameInfo game;
local DBRecord nextRecord;
local array allRecords;
packageName = _.text.IntoString(packageToErase);
if (packageName == "") {
return;
}
game = __level().unreal_api().GetGameType();
game.DeletePackage(packageName);
// Delete any leftover objects. This has to be done *after*
// `DeletePackage()` call, otherwise removed garbage can reappear.
// No clear idea why it works this way.
foreach game.AllDataObjects(class'DBRecord', nextRecord, packageName) {
allRecords[allRecords.length] = nextRecord;
}
for (i = 0; i < allRecords.length; i += 1)
{
game.DeleteDataObject( class'DBRecord', string(allRecords[i].name),
packageName);
}
}
/**
* Returns array of names of all available local databases.
*
* @return List of names of all local databases.
*/
public final function array ListLocal()
{
local int i;
local array dbNames;
local array dbNamesAsStrings;
dbNamesAsStrings = GetPerObjectNames( "AcediaDB",
string(class'LocalDatabase'.name),
MaxInt);
for (i = 0; i < dbNamesAsStrings.length; i += 1) {
dbNames[dbNames.length] = _.text.FromString(dbNamesAsStrings[i]);
}
return dbNames;
}
defaultproperties
{
localDBClass = class'LocalDatabaseInstance'
infoLocalDatabaseCreated = (l=LOG_Info,m="Local database \"%1\" was created.")
infoLocalDatabaseLoaded = (l=LOG_Info,m="Local database \"%1\" was loaded.")
infoLocalDatabaseDeleted = (l=LOG_Info,m="Local database \"%1\" was deleted.")
}