/** * API that provides methods for creating/destroying and managing available * databases. * Copyright 2021 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 AssociativeArray loadedLocalDatabases; private final function CreateLocalDBMapIfMissing() { if (loadedLocalDatabases == none) { loadedLocalDatabases = __().collections.EmptyAssociativeArray(); } } /** * 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(Text 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(Text 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; } /** * 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(Text databaseName) { local DBRecord rootRecord; local Text rootRecordName; local LocalDatabase newConfig; local LocalDatabaseInstance newLocalDBInstance; CreateLocalDBMapIfMissing(); // 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()) return none; if (loadedLocalDatabases.HasKey(databaseName)) return none; newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass)); loadedLocalDatabases.SetItem(databaseName.Copy(), newLocalDBInstance); rootRecord = class'DBRecord'.static.NewRecord(databaseName); rootRecordName = _.text.FromString(string(rootRecord.name)); newConfig.SetRootName(rootRecordName); newConfig.Save(); newLocalDBInstance.Initialize(newConfig, rootRecord); _.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(Text databaseName) { local DBRecord rootRecord; local Text rootRecordName; local LocalDatabase newConfig; local LocalDatabaseInstance newLocalDBInstance; 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)); loadedLocalDatabases.SetItem(databaseName.Copy(), newLocalDBInstance); 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); _.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(Text databaseName) { return LoadLocal(databaseName) != none; } /** * 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(Text databaseName) { local LocalDatabase localDatabaseConfig; local LocalDatabaseInstance localDatabase; local AssociativeArray.Entry dbEntry; CreateLocalDBMapIfMissing(); // To delete database we first need to load it localDatabase = LoadLocal(databaseName); if (localDatabase != none) { localDatabaseConfig = localDatabase.GetConfig(); } 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(); return true; } return false; } private function EraseAllPackageData(Text packageToErase) { local int i; local string packageName; local GameInfo game; local DBRecord nextRecord; local array allRecords; packageName = _.text.ToString(packageToErase); if (packageName == "") { return; } game = _.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' }