/** * 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 `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; local Text dbKey; if (databaseName == none) { return none; } CreateLocalDBMapIfMissing(); if (loadedLocalDatabases.HasKey(databaseName)) { return LocalDatabaseInstance(loadedLocalDatabases .GetItem(databaseName)); } // No need to check `databaseName` for being valid, // since `Load()` will just return `none` if it is not. newConfig = class'LocalDatabase'.static.Load(databaseName); if (newConfig == none) { return none; } if (!newConfig.HasDefinedRoot() && !newConfig.ShouldCreateIfMissing()) { return none; } newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass)); dbKey = databaseName.Copy(); loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance); dbKey.FreeSelf(); if (newConfig.HasDefinedRoot()) { rootRecordName = newConfig.GetRootName(); rootRecord = class'DBRecord'.static .LoadRecord(rootRecordName, databaseName); } else { rootRecord = class'DBRecord'.static.NewRecord(databaseName); rootRecordName = _.text.FromString(string(rootRecord.name)); newConfig.SetRootName(rootRecordName); newConfig.Save(); } newLocalDBInstance.Initialize(newConfig, rootRecord); _.logger.Auto(infoLocalDatabaseLoaded).Arg(databaseName.Copy()); _.memory.Free(rootRecordName); _.memory.Free(newLocalDBInstance); 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 = _server.unreal.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.") }