package mi.hdm.filesystem; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import mi.hdm.TastyPages; import mi.hdm.mealPlan.MealPlan; import mi.hdm.recipes.*; import mi.hdm.shoppingList.ShoppingList; import mi.hdm.typeAdapters.CategoryManagerTypeAdapter; import mi.hdm.typeAdapters.MealPlanTypeAdapter; import mi.hdm.typeAdapters.RecipeTypeAdapter; import mi.hdm.typeAdapters.ShoppingListTypeAdapter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.math.BigDecimal; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Containing methods to serialize and deserialize parts of the app content for saving to / loading from the filesystem */ public class FileManager { private final static String PATH_TO_DEFAULT_INGREDIENTS = "/data/simple_nutrition.csv"; private final static String PATH_TO_DEMO_RECIPES = "/recipes/"; private final static String PATH_TO_DEMO_CATEGORIES = "/data/categories.json"; private final static String FOLDER_NAME = "TastyPages"; private final static Path PATH_TO_USER_DATA = Path.of(System.getProperty("user.home"), FOLDER_NAME); private final static Logger log = LogManager.getLogger(FileManager.class); public static void serializeToFile(TastyPages app) { log.info("Writing user data to '{}'", PATH_TO_USER_DATA); //Serialize ingredient manager serializeIngredientManager(app.getIngredientManager(), PATH_TO_USER_DATA.resolve("ingredients.csv")); serializeRecipeManager(app.getRecipeManager(), PATH_TO_USER_DATA.resolve("recipes")); serializeCategoryManager(app.getCategoryManager(), PATH_TO_USER_DATA.resolve("categories.json")); serializeMealPlan(app.getMealPlan(), PATH_TO_USER_DATA.resolve("mealplan.json")); serializeShoppingList(app.getShoppingList(), app.getRecipeManager(), PATH_TO_USER_DATA.resolve("shoppingList.json")); } @SuppressWarnings("ResultOfMethodCallIgnored") public static TastyPages deserializeFromFilesystem() throws IOException { log.info("Trying to read {}", PATH_TO_USER_DATA); if (PATH_TO_USER_DATA.toFile().mkdirs()) { //If .mkdirs() returns true, that means the folder has not existed before -> in this case, only load default ingredients! log.info("TastyPages folder has been created at {}", PATH_TO_USER_DATA); IngredientManager deserializedIngredientManager = deserializeIngredientManager(getAbsolutePathFromResourceFolder(FileManager.PATH_TO_DEFAULT_INGREDIENTS)); RecipeManager recipeManager = deserializeRecipeManager(Path.of(getAbsolutePathFromResourceFolder(FileManager.PATH_TO_DEMO_RECIPES))); String fileContent = Files.readString(Path.of(FileManager.getAbsolutePathFromResourceFolder(FileManager.PATH_TO_DEMO_CATEGORIES))); CategoryManager categoryManager = deserializeCategoryManager(fileContent); return new TastyPages(deserializedIngredientManager, recipeManager, categoryManager); } else { //otherwise, read user data log.info("Found TastyPages folder at {}, loading user data from there.", PATH_TO_USER_DATA); IngredientManager ingredientManager; RecipeManager recipeManager; CategoryManager categoryManager; MealPlan mealPlan; ShoppingList shoppingList; //Deserialize ingredient manager { Path toIngredients = PATH_TO_USER_DATA.resolve("ingredients.csv"); if (Files.exists(toIngredients)) { log.info("Loading ingredients from '{}'", toIngredients); ingredientManager = deserializeIngredientManager(toIngredients.toString()); } else { log.info("Could not find /ingredients.csv, falling back to default ingredients."); ingredientManager = deserializeIngredientManager(getAbsolutePathFromResourceFolder(FileManager.PATH_TO_DEFAULT_INGREDIENTS)); } } //Deserialize recipe manager { Path recipeFolder = PATH_TO_USER_DATA.resolve("recipes"); if (Files.exists(recipeFolder) && Files.isDirectory(recipeFolder)) { log.info("Loading recipes from '{}'", recipeFolder); recipeManager = deserializeRecipeManager(recipeFolder); } else { log.info("Could not find recipe folder, creating empty instance of RecipeManger"); recipeFolder.toFile().mkdir(); recipeManager = deserializeRecipeManager(Path.of(getAbsolutePathFromResourceFolder(FileManager.PATH_TO_DEMO_RECIPES))); } } //Deserialize category manager { Path toCategoryJson = PATH_TO_USER_DATA.resolve("categories.json"); if (Files.exists(toCategoryJson) && Files.isRegularFile(toCategoryJson)) { log.info("Loading categories from '{}'", toCategoryJson); String fileContent = Files.readString(toCategoryJson); categoryManager = deserializeCategoryManager(fileContent); } else { log.info("Could not find /categories.json, creating empty instance of CategoryManager"); String fileContent = Files.readString(Path.of(FileManager.getAbsolutePathFromResourceFolder(FileManager.PATH_TO_DEMO_CATEGORIES))); categoryManager = deserializeCategoryManager(fileContent); /*toCategoryJson.toFile().createNewFile(); categoryManager = new CategoryManager();*/ } } //Deserialize meal plan { Path toMealPlanJson = PATH_TO_USER_DATA.resolve("mealplan.json"); if (Files.exists(toMealPlanJson) && Files.isRegularFile(toMealPlanJson)) { log.info("Loading meal plan from '{}'", toMealPlanJson); String fileContent = Files.readString(toMealPlanJson); mealPlan = deserializeMealPlan(fileContent, recipeManager); } else { log.info("Could not find /mealplan.json, creating empty instance of MealPlan"); toMealPlanJson.toFile().createNewFile(); mealPlan = new MealPlan(recipeManager); } } //Deserialize shopping list { Path toShoppingListJson = PATH_TO_USER_DATA.resolve("shoppingList.json"); if (Files.exists(toShoppingListJson) && Files.isRegularFile(toShoppingListJson)) { log.info("Loading shopping list from '{}'", toShoppingListJson); String fileContent = Files.readString(toShoppingListJson); shoppingList = deserializeShoppingList(fileContent, recipeManager); } else { log.info("Could not find /shoppingList.json, creating empty shopping list"); toShoppingListJson.toFile().createNewFile(); shoppingList = new ShoppingList(recipeManager); } } return new TastyPages(recipeManager, ingredientManager, categoryManager, mealPlan, shoppingList); } } private static IngredientManager deserializeIngredientManager(String path) throws IOException { CSVParser parser = new CSVParser(); List<Ingredient> ingredients = parser.getIngredientsFromCSV( path, ',', "name", "calories", "carbohydrate", "fat", "protein", "fiber", "sodium" ); return new IngredientManager(ingredients); } private static void serializeIngredientManager(IngredientManager ingredientManager, Path path) { log.info("Writing ingredients to '{}'", path.getFileName()); try (BufferedWriter writer = new BufferedWriter(new FileWriter(path.toFile()))) { writer.write("name,calories,carbohydrate,fat,protein,fiber,sodium\n"); final List<Ingredient> ingredients = ingredientManager.getAllIngredients().values().stream().toList(); for (int i = 0; i < ingredients.size(); i++) { if (i % 200 == 0) writer.flush(); //flush the writer regularly writer.write(ingredientToCSV(ingredients.get(i))); } } catch (IOException e) { log.error("Exception when writing ingredients to file!"); e.printStackTrace(); throw new RuntimeException(e); } } private static RecipeManager deserializeRecipeManager(Path path) throws IOException { List<Path> recipePaths; try (Stream<Path> stream = Files.list(path)) { recipePaths = stream.toList(); } List<Recipe> recipes = recipePaths .stream() .map(p -> { try { return Files.readString(p); } catch (IOException e) { e.printStackTrace(); log.error("Could not read file contents for {}. Check file permissions and content", p); return null; } }) .filter(Objects::nonNull) .map(FileManager::JSONtoRecipe) .toList(); return new RecipeManager(recipes); } private static void serializeRecipeManager(RecipeManager recipeManager, Path path) { String[] recipeFolderContent = path.toFile().list(); if (recipeFolderContent == null) { //wrong path provided log.fatal("Path to recipe folder is NOT a folder! '{}' Unable to save recipes.", path); return; } List<String> toDelete = Arrays.stream(recipeFolderContent).collect(Collectors.toCollection(ArrayList::new)); log.info("Writing all recipes to /recipes"); recipeManager.getRecipes() .forEach(recipe -> { try { String recipeName = recipe.getName() + ".json"; String recipePath = path + "\\" + recipeName; Files.writeString(Path.of(recipePath), recipeToJSON(recipe)); log.debug("Wrote recipe '{}' to '{}'", recipe.getName(), recipePath); toDelete.remove(recipeName); } catch (IOException e) { log.error("Error writing recipe '{}' to file. Recipe will be lost.", recipe.getName()); e.printStackTrace(); } } ); long deleted = toDelete.stream() .filter(filename -> { try { return Files.deleteIfExists(Path.of(path + "\\" + filename)); } catch (IOException e) { log.warn("Could not delete '{}': {}", filename, e.getMessage()); return false; } }) .count(); log.info("Deleted {} recipes from the filesystem", deleted); } private static CategoryManager deserializeCategoryManager(String json) { if (json.isBlank()) { log.info("JSON file is blank, creating empty instance of Category Manager"); return new CategoryManager(); } Gson gson = new GsonBuilder().registerTypeAdapter(CategoryManager.class, new CategoryManagerTypeAdapter()).create(); return gson.fromJson(json, CategoryManager.class); } private static void serializeCategoryManager(CategoryManager categoryManager, Path path) { log.info("Writing categories to categories.json"); Gson gson = new GsonBuilder() .registerTypeAdapter(CategoryManager.class, new CategoryManagerTypeAdapter()) .create(); String json = gson.toJson(categoryManager); try { Files.writeString(path, json); } catch (IOException io) { log.error("Error writing categories to file."); io.printStackTrace(); throw new RuntimeException(); } } private static MealPlan deserializeMealPlan(String json, RecipeManager recipeManager) { if (json.isBlank()) { log.info("JSON file is blank, creating empty instance of Meal Plan"); return new MealPlan(recipeManager); } Gson gson = new GsonBuilder() .registerTypeAdapter(MealPlan.class, new MealPlanTypeAdapter(recipeManager)) .create(); return gson.fromJson(json, MealPlan.class); } private static void serializeMealPlan(MealPlan mealPlan, Path path) { Gson gson = new GsonBuilder(). registerTypeAdapter(MealPlan.class, new MealPlanTypeAdapter(null)) .create(); String json = gson.toJson(mealPlan); try { Files.writeString(path, json); } catch (IOException io) { log.error("Error writing meal plan to file."); io.printStackTrace(); throw new RuntimeException(); } } private static ShoppingList deserializeShoppingList(String json, RecipeManager recipeManager) { if (json.isBlank()) { log.info("JSON file is blank, creating empty instance of Shopping List"); return new ShoppingList(recipeManager); } Gson gson = new GsonBuilder() .registerTypeAdapter(ShoppingList.class, new ShoppingListTypeAdapter(recipeManager)) .create(); return gson.fromJson(json, ShoppingList.class); } private static void serializeShoppingList(ShoppingList shoppingList, RecipeManager recipeManager, Path path) { Gson gson = new GsonBuilder() .registerTypeAdapter(ShoppingList.class, new ShoppingListTypeAdapter(recipeManager)) .create(); String json = gson.toJson(shoppingList); try { Files.writeString(path, json); } catch (IOException io) { log.error("Error writing shopping list to file."); io.printStackTrace(); throw new RuntimeException(); } } private static String recipeToJSON(Recipe recipe) { Gson gson = new GsonBuilder() .registerTypeAdapter(Recipe.class, new RecipeTypeAdapter()) .create(); return gson.toJson(recipe); } /** * Used to convert an ingredient into a CSV String seperated by comma. Floating point numbers * will use dots as decimal points * * @return a comma seperated String containing the name of the ingredient as well as the contents of its nutrition table */ private static String ingredientToCSV(Ingredient ingredient) { //"name", "calories", "carbohydrate", "fat", "protein", "fiber", "sodium" Map<Nutrition, BigDecimal> map = ingredient.getNutritionTable().getTable(); return String.format( Locale.ENGLISH, "\"%s\", %f, %f, %f, %f, %f, %f%n", ingredient.getName(), map.get(Nutrition.CALORIES).doubleValue(), map.get(Nutrition.CARBS).doubleValue(), map.get(Nutrition.FAT).doubleValue(), map.get(Nutrition.PROTEINS).doubleValue(), map.get(Nutrition.FIBERS).doubleValue(), map.get(Nutrition.SALT).doubleValue() ); } private static Recipe JSONtoRecipe(String json) { Gson gson = new GsonBuilder().registerTypeAdapter(Recipe.class, new RecipeTypeAdapter()).create(); return gson.fromJson(json, Recipe.class); } public static String getAbsolutePathFromResourceFolder(String p) { try { URL resourceUrl = FileManager.class.getResource(p); assert resourceUrl != null; Path path = Paths.get(resourceUrl.toURI()); return path.toFile().getAbsolutePath(); } catch (URISyntaxException e) { log.error("Absolute path for \"{}\" not found", p); return null; } } }