using Models; using System.Collections.Immutable; using System.Diagnostics; using System.Text; using System.Text.Json; namespace LocalServices.Data { /// /// Database implementation with catastrophic performances. /// This database implementation persists data in json and will save all the data in their files for each mutable requests. /// public class CatastrophicPerformancesDatabase : IDatabase { private static readonly string RECIPES_FILENAME = "recipes_data.xml"; private static readonly string USERS_FILENAME = "users_data.xml"; private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml"; private readonly Dictionary recipesData; private readonly Dictionary usersData; private readonly Dictionary accountsData; private readonly string dbPath; public CatastrophicPerformancesDatabase(string folderPath) { dbPath = folderPath; if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); usersData = Load(USERS_FILENAME); recipesData = Load(RECIPES_FILENAME); accountsData = Load(ACCOUNTS_FILENAME); } public bool IsEmpty() { return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0; } public Account? GetAccount(string email, string passwordHash) { if (!accountsData.TryGetValue(email, out AccountData? data)) return null; if (data.PasswordHash != passwordHash) return null; return new Account(usersData[data.UserId].User, data.Email); } public void InsertAccount(Account account, string passwordHash) { accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash); Save(ACCOUNTS_FILENAME, accountsData); InsertUser(account.User); } public Recipe? GetRecipe(Guid id) { if (recipesData.TryGetValue(id, out RecipeData? data)) return ConvertRecipeDataToRecipe(data); return null; } public RecipeRate GetRecipeRate(Guid user, Guid recipe) { return usersData[user].Rates[recipe]; } public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount) { usersData[userId].RecipesList[recipeId] = persAmount; Save(USERS_FILENAME, usersData); } public void RemoveFromUserList(Guid userId, Guid recipeId) { usersData[userId].RecipesList.Remove(recipeId); Save(USERS_FILENAME, usersData); } public void InsertRecipe(Recipe recipe) { recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps); Save(RECIPES_FILENAME, recipesData); } public void InsertUser(User user) { usersData[user.Id] = new UserData(user, new Dictionary(), new Dictionary()); Save(USERS_FILENAME, usersData); } public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate) { usersData[userId].Rates[recipeId] = rate; Save(USERS_FILENAME, usersData); } public void RemoveRecipe(Guid id) { recipesData.Remove(id); Save(RECIPES_FILENAME, recipesData); } public ImmutableList ListAllRecipes() { return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe); } public ImmutableDictionary ListRatesOf(Guid user) { return usersData[user].Rates.ToImmutableDictionary(); } public ImmutableDictionary GetRecipeListOf(Guid user) { return usersData[user].RecipesList.ToImmutableDictionary(); } private Recipe ConvertRecipeDataToRecipe(RecipeData rd) { var owner = usersData[rd.OwnerID].User; return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps); } private Dictionary Load(string fileName) { var file = dbPath + "/" + fileName; var fileInfo = new FileInfo(file); if (!fileInfo.Exists) fileInfo.Create(); if (fileInfo.Length == 0) return new Dictionary(); //file is empty thus there is nothing to deserialize string text = File.ReadAllText(file); return JsonSerializer.Deserialize>(text); } private async void Save(string fileName, Dictionary dict) { string json = JsonSerializer.Serialize(dict); using (var stream = WaitForFile(dbPath + "/" + fileName, FileMode.Open, FileAccess.Write, FileShare.Write)) { var bytes = Encoding.ASCII.GetBytes(json); await stream.WriteAsync(bytes, 0, bytes.Length); } } // This is a workaround function to wait for a file to be released before opening it. // This was to fix the Save method that used to throw sometimes as the file were oftenly being scanned by the androids' antivirus. // Simply wait until the file is released and return it. This function will never return until private static FileStream WaitForFile(string fullPath, FileMode mode, FileAccess access, FileShare share) { for (int attempt = 0 ; attempt < 40; attempt++) { FileStream? fs = null; try { fs = new FileStream(fullPath, mode, access, share); return fs; } catch (FileNotFoundException e) { throw e; } catch (IOException e) { Debug.WriteLine(e.Message + " in thread " + Thread.CurrentThread.Name + " " + Thread.CurrentThread.ManagedThreadId); if (fs != null) fs.Dispose(); Thread.Sleep(200); } } throw new TimeoutException("Could not access file '" + fullPath + "', maximum attempts reached."); } } }