From 554c026b279377e7374d87329dedd0db9a104477 Mon Sep 17 00:00:00 2001 From: Lukas Karsch <lk224@hdm-stuttgart.de> Date: Wed, 24 May 2023 17:02:18 +0200 Subject: [PATCH] Categories, recipes and ingredients now saved in hashmap together with their associated code and a whole bunch of refactoring to go along with it. TypeAdapters for Recipes, NutritionTables and Categories. Saving and retrieving Recipe, NutritionTable and Category to / from JSON works (see tests). --- pom.xml | 6 + src/main/java/mi/hdm/TastyPages.java | 40 ++++- .../java/mi/hdm/components/RecipeVbox.java | 17 ++- .../mi/hdm/controllers/HeaderController.java | 13 +- .../hdm/controllers/MainPageController.java | 8 +- src/main/java/mi/hdm/controllers/View.java | 8 +- .../mi/hdm/controllers/ViewComponent.java | 4 +- .../java/mi/hdm/filesystem/CSVParser.java | 6 + .../java/mi/hdm/filesystem/FileManager.java | 143 +++++++++++++++++- src/main/java/mi/hdm/recipes/Category.java | 20 ++- .../java/mi/hdm/recipes/CategoryManager.java | 42 +++-- src/main/java/mi/hdm/recipes/Ingredient.java | 16 ++ .../mi/hdm/recipes/IngredientManager.java | 45 ++++-- .../mi/hdm/recipes/NutritionCalculator.java | 2 - .../java/mi/hdm/recipes/NutritionTable.java | 12 +- src/main/java/mi/hdm/recipes/Recipe.java | 82 +++++++--- .../java/mi/hdm/recipes/RecipeComponent.java | 7 + .../java/mi/hdm/recipes/RecipeManager.java | 33 ++-- .../java/mi/hdm/recipes/RecipeSearch.java | 22 ++- .../mi/hdm/shoppingList/ShoppingList.java | 62 ++++---- .../hdm/typeAdapters/CategoryTypeAdapter.java | 53 +++++++ .../NutritionTableTypeAdapter.java | 52 +++++++ .../hdm/typeAdapters/RecipeTypeAdapter.java | 107 +++++++++++++ src/main/java/module-info.java | 3 +- src/main/resources/log4j2.xml | 2 +- src/test/java/mi/hdm/filesystem/JsonTest.java | 61 ++++++++ .../java/mi/hdm/recipes/RecipeSearchTest.java | 15 +- src/test/java/mi/hdm/recipes/RecipeTest.java | 4 +- .../java/mi/hdm/recipes/ValidObjectsPool.java | 5 +- .../mi/hdm/shoppingList/ShoppingListTest.java | 18 +-- 30 files changed, 755 insertions(+), 153 deletions(-) create mode 100644 src/main/java/mi/hdm/typeAdapters/CategoryTypeAdapter.java create mode 100644 src/main/java/mi/hdm/typeAdapters/NutritionTableTypeAdapter.java create mode 100644 src/main/java/mi/hdm/typeAdapters/RecipeTypeAdapter.java create mode 100644 src/test/java/mi/hdm/filesystem/JsonTest.java diff --git a/pom.xml b/pom.xml index 0e8382b..19e61cf 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,12 @@ <version>2.17.1</version> </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>2.10.1</version> + </dependency> + </dependencies> <build> diff --git a/src/main/java/mi/hdm/TastyPages.java b/src/main/java/mi/hdm/TastyPages.java index 1fc5261..bad0adf 100644 --- a/src/main/java/mi/hdm/TastyPages.java +++ b/src/main/java/mi/hdm/TastyPages.java @@ -5,6 +5,7 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; import mi.hdm.controllers.View; +import mi.hdm.filesystem.FileManager; import mi.hdm.mealPlan.MealPlan; import mi.hdm.recipes.*; import mi.hdm.shoppingList.ShoppingList; @@ -22,17 +23,50 @@ public class TastyPages extends Application { public final static ShoppingList shoppingList = new ShoppingList(); public final static CategoryManager categoryManager = new CategoryManager(List.of(new Category("Mittagessen", 0xFF4040), new Category("mjam", 0x00FF00))); public final static MealPlan mealPlan = new MealPlan(new HashMap<>()); + public final static IngredientManager ingredientManager = new IngredientManager(); private final static Logger log = LogManager.getLogger(TastyPages.class); private static Stage stage; + /** + * Creates a new instance of the application holding all user data. The parameters are expected to be parsed with the FileManager + */ + public TastyPages(RecipeManager recipeManager, IngredientManager ingredientManager, CategoryManager categoryManager, MealPlan mealPlan, ShoppingList shoppingList) { + //TODO + } + + /** + * Creates a new instance of the application. All services will be empty when instantiated, except for the ingredientManager which + * is expected to hold the default ingredients that exist inside the resources folder + */ + public TastyPages(IngredientManager ingredientManager) { + //TODO create new and empty instances of every dependency (except ingredient manager) + } + + /** + * Creates a new, completely empty instance of the application. This constructor is not expected to be used. + */ + public TastyPages() { + } + public static void main(String[] args) { log.info("Starting TastyPages"); + try { + TastyPages app = FileManager.deserializeFromFile(); + assert app != null; + app.startApplication(args); + } catch (Exception e) { + log.fatal("Exception: check your filepaths!"); + e.printStackTrace(); + System.exit(1); + } + //create instances of every dependency -> load ingredients + recipeManager.addRecipe(new Recipe("Mein erstes Rezept", Map.of(new Ingredient(Measurement.GRAM, "Mehl", new NutritionTable(100, 20, 8, 14, 2.5, 3)), 100), "Description", List.of("joa"), List.of(categoryManager.getAllCategories().get(1).getCode()), 40)); + recipeManager.addRecipe(new Recipe("Mein letztes Rezept", Map.of(new Ingredient(Measurement.GRAM, "Zucker", new NutritionTable(100, 500, 8, 14, 2.5, 3)), 100), "Description", List.of("joa"), List.of(categoryManager.getAllCategories().get(0).getCode()), 40)); + } - recipeManager.addRecipe(new Recipe("Mein erstes Rezept", Map.of(new Ingredient(Measurement.GRAM, "Mehl", new NutritionTable(100, 20, 8, 14, 2.5, 3)), 100), "Description", List.of("joa"), List.of(categoryManager.getAllCategories().get(1)), 40)); - recipeManager.addRecipe(new Recipe("Mein letztes Rezept", Map.of(new Ingredient(Measurement.GRAM, "Zucker", new NutritionTable(100, 500, 8, 14, 2.5, 3)), 100), "Description", List.of("joa"), List.of(categoryManager.getAllCategories().get(0)), 40)); - + public void startApplication(String[] args) { launch(args); } diff --git a/src/main/java/mi/hdm/components/RecipeVbox.java b/src/main/java/mi/hdm/components/RecipeVbox.java index c393f6b..7577123 100644 --- a/src/main/java/mi/hdm/components/RecipeVbox.java +++ b/src/main/java/mi/hdm/components/RecipeVbox.java @@ -5,6 +5,7 @@ import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import mi.hdm.recipes.Category; +import mi.hdm.recipes.CategoryManager; import mi.hdm.recipes.Recipe; import java.util.ArrayList; @@ -16,7 +17,7 @@ import java.util.List; public class RecipeVbox extends VBox { private final Recipe recipe; - public RecipeVbox(Recipe recipe) { + public RecipeVbox(Recipe recipe, CategoryManager categoryManager) { super(); this.recipe = recipe; @@ -30,20 +31,20 @@ public class RecipeVbox extends VBox { List<Label> categoryLabels = new ArrayList<>(); //add a maximum of 3 categories to the recipe tile - for (int i = 0; i < Math.min(recipe.getCategories().size(), 3); i++) { - Category c = recipe.getCategories().get(i); - Label l = new Label(c.getName()); + List<Category> categories = categoryManager.getCategoriesFromKeys(recipe.getCategoryCodes()); + for (int i = 0; i < Math.min(recipe.getCategoryCodes().size(), 3); i++) { + Label l = new Label(categories.get(i).getName()); //TODO: make border radius work on the styling - l.setStyle("-fx-background-color: " + c.getColourCode() + ";" + + l.setStyle("-fx-background-color: " + categories.get(i).getColourCode() + ";" + "-fx-padding: 3px;" + "-fx-border-radius: 5px;" ); categoryLabels.add(l); } - HBox categories = new HBox(); - categories.getChildren().addAll(categoryLabels); + HBox categoryHbox = new HBox(); + categoryHbox.getChildren().addAll(categoryLabels); - this.getChildren().addAll(recipeName, recipeDescription, categories); + this.getChildren().addAll(recipeName, recipeDescription, categoryHbox); this.setPrefWidth(180); this.setPrefHeight(250); diff --git a/src/main/java/mi/hdm/controllers/HeaderController.java b/src/main/java/mi/hdm/controllers/HeaderController.java index e302f68..9953e2a 100644 --- a/src/main/java/mi/hdm/controllers/HeaderController.java +++ b/src/main/java/mi/hdm/controllers/HeaderController.java @@ -2,14 +2,14 @@ package mi.hdm.controllers; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import mi.hdm.recipes.IngredientManager; import mi.hdm.recipes.Recipe; import mi.hdm.recipes.RecipeManager; import mi.hdm.recipes.RecipeSearch; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.List; - +import java.util.Collection; public class HeaderController extends BaseController { private final static Logger log = LogManager.getLogger(HeaderController.class); @@ -18,10 +18,13 @@ public class HeaderController extends BaseController { private TextField searchBox; private final RecipeManager recipeManager; - private List<Recipe> lastSearchResults; + private final IngredientManager ingredientManager; + + private Collection<Recipe> lastSearchResults; - public HeaderController(RecipeManager recipeManager) { + public HeaderController(RecipeManager recipeManager, IngredientManager ingredientManager) { this.recipeManager = recipeManager; + this.ingredientManager = ingredientManager; lastSearchResults = recipeManager.getRecipes(); } @@ -44,7 +47,7 @@ public class HeaderController extends BaseController { public void searchByQuery() { String query = searchBox.getText(); log.debug("User submitted search box"); - RecipeSearch recipeSearch = new RecipeSearch(recipeManager.getRecipes()); + RecipeSearch recipeSearch = new RecipeSearch(recipeManager, ingredientManager, recipeManager.getRecipes()); lastSearchResults = recipeSearch.searchByQuery(query); System.out.println("Results: "); lastSearchResults.forEach(result -> System.out.println(" " + result.getName())); diff --git a/src/main/java/mi/hdm/controllers/MainPageController.java b/src/main/java/mi/hdm/controllers/MainPageController.java index 14a25a6..ab47313 100644 --- a/src/main/java/mi/hdm/controllers/MainPageController.java +++ b/src/main/java/mi/hdm/controllers/MainPageController.java @@ -18,6 +18,7 @@ public class MainPageController extends BaseController { private final static Logger log = LogManager.getLogger(MainPageController.class); private final RecipeManager recipeManager; + private final IngredientManager ingredientManager; private final CategoryManager categoryManager; private final List<CategoryCheckBox> categoryCheckboxesInGui = new ArrayList<>(); @@ -30,8 +31,9 @@ public class MainPageController extends BaseController { @FXML private HBox categories; - public MainPageController(RecipeManager recipeManager, CategoryManager categoryManager) { + public MainPageController(RecipeManager recipeManager, IngredientManager ingredientManager, CategoryManager categoryManager) { this.recipeManager = recipeManager; + this.ingredientManager = ingredientManager; this.categoryManager = categoryManager; } @@ -53,7 +55,7 @@ public class MainPageController extends BaseController { currentRecipesInGUI.clear(); //remove all recipes... for (Recipe recipe : recipes) { - RecipeVbox recipeVbox = new RecipeVbox(recipe); + RecipeVbox recipeVbox = new RecipeVbox(recipe, categoryManager); recipeVbox.setOnMouseClicked(mouseEvent -> System.out.format("User selected '%s'.%n", recipe.getName())); //TODO: this needs to display the recipe view currentRecipesInGUI.add(recipeVbox); } @@ -80,7 +82,7 @@ public class MainPageController extends BaseController { .filter(CheckBox::isSelected) .map(CategoryCheckBox::getAssociatedCategory) .toList(); - RecipeSearch recipeSearch = new RecipeSearch(recipeManager.getRecipes()); + RecipeSearch recipeSearch = new RecipeSearch(recipeManager, ingredientManager, recipeManager.getRecipes()); return recipeSearch.searchByCategory(currentlySelectedCategories); } } diff --git a/src/main/java/mi/hdm/controllers/View.java b/src/main/java/mi/hdm/controllers/View.java index fe0283b..5139258 100644 --- a/src/main/java/mi/hdm/controllers/View.java +++ b/src/main/java/mi/hdm/controllers/View.java @@ -5,6 +5,7 @@ import javafx.scene.Parent; import mi.hdm.TastyPages; import mi.hdm.mealPlan.MealPlan; import mi.hdm.recipes.CategoryManager; +import mi.hdm.recipes.IngredientManager; import mi.hdm.recipes.RecipeManager; import mi.hdm.shoppingList.ShoppingList; import org.apache.logging.log4j.LogManager; @@ -24,6 +25,7 @@ public enum View { private final ShoppingList shoppingList = TastyPages.shoppingList; private final MealPlan mealPlan = TastyPages.mealPlan; private final CategoryManager categoryManager = TastyPages.categoryManager; + private final IngredientManager ingredientManager = TastyPages.ingredientManager; private final String path; private final String windowTitle; @@ -42,7 +44,7 @@ public enum View { //set the correct controller factory for views (necessary to enable dependency injection) switch (this) { case MAIN -> loader.setControllerFactory((callback) -> - new MainPageController(recipeManager, categoryManager) + new MainPageController(recipeManager, ingredientManager, categoryManager) ); case RECIPE_VIEW -> loader.setControllerFactory((callback) -> @@ -65,4 +67,8 @@ public enum View { public String getWindowTitle() { return windowTitle; } + + public String getPath() { + return path; + } } diff --git a/src/main/java/mi/hdm/controllers/ViewComponent.java b/src/main/java/mi/hdm/controllers/ViewComponent.java index 940c70e..8788c33 100644 --- a/src/main/java/mi/hdm/controllers/ViewComponent.java +++ b/src/main/java/mi/hdm/controllers/ViewComponent.java @@ -3,6 +3,7 @@ package mi.hdm.controllers; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import mi.hdm.TastyPages; +import mi.hdm.recipes.IngredientManager; import mi.hdm.recipes.RecipeManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,6 +19,7 @@ public enum ViewComponent { //dependencies private final RecipeManager recipeManager = TastyPages.recipeManager; + private final IngredientManager ingredientManager = TastyPages.ingredientManager; ViewComponent(String path) { this.path = path; @@ -30,7 +32,7 @@ public enum ViewComponent { switch (this) { case HEADER -> loader.setControllerFactory((callback) -> - new HeaderController(recipeManager) + new HeaderController(recipeManager, ingredientManager) ); } diff --git a/src/main/java/mi/hdm/filesystem/CSVParser.java b/src/main/java/mi/hdm/filesystem/CSVParser.java index c3c9c65..ec0f748 100644 --- a/src/main/java/mi/hdm/filesystem/CSVParser.java +++ b/src/main/java/mi/hdm/filesystem/CSVParser.java @@ -28,6 +28,11 @@ public class CSVParser { public List<Ingredient> getIngredientsFromCSV(String filepath, char split, String... extract) throws IOException { log.info("Trying to read ingredients from CSV: {}", filepath); + if (extract.length == 0) { + log.fatal("Arguments for parser are incorrect."); + throw new InvalidPropertiesFormatException("Arguments to extract are length 0"); + } + //try-block with automatic resource management try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) { final String header = reader.readLine(); //read first line of CSV @@ -56,6 +61,7 @@ public class CSVParser { } private Ingredient getIngredientFromLine(String line, char split, int[] idx) throws NumberFormatException { + //TODO: take into account the measurement of the ingredient when reading it log.debug("Trying to parse line {}", line); final Measurement measurement = Measurement.GRAM; diff --git a/src/main/java/mi/hdm/filesystem/FileManager.java b/src/main/java/mi/hdm/filesystem/FileManager.java index 414b37f..706869f 100644 --- a/src/main/java/mi/hdm/filesystem/FileManager.java +++ b/src/main/java/mi/hdm/filesystem/FileManager.java @@ -1,24 +1,153 @@ 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.RecipeTypeAdapter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +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.List; +import java.util.Map; +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 static final FileManager fileManager = new FileManager(); + private final static String PATH_TO_DEFAULT_INGREDIENTS = "/data/nutrition.csv"; + private final static String PATH_TO_USER_DATA = System.getProperty("user.home") + "\\AppData\\Roaming\\TastyPages"; + + private final static Logger log = LogManager.getLogger(FileManager.class); + + private final static Gson gson = new Gson(); - public static FileManager getInstance() { - return fileManager; + public static void serializeToFile(TastyPages app) { } - public void serializeToFile(TastyPages app) { + public static TastyPages deserializeFromFile() throws Exception { //TODO: if this fails, log.fatal and System.exit + log.info("Trying to read {}", PATH_TO_USER_DATA); + Path userPath = Path.of(PATH_TO_USER_DATA); + if (userPath.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 {}", userPath); + IngredientManager deserializedIngredientManager = deserializeIngredientManager(getAbsolutePathFromResourceFolder(PATH_TO_DEFAULT_INGREDIENTS)); + return new TastyPages(deserializedIngredientManager); + } else { //otherwise, read user data + log.info("Found TastyPages folder at {}, loading user data from there.", userPath); + IngredientManager ingredientManager; + RecipeManager recipeManager; + CategoryManager categoryManager; + MealPlan mealPlan; + ShoppingList shoppingList; + + if (Files.exists(Path.of(userPath + "ingredients.csv"))) { + ingredientManager = deserializeIngredientManager(PATH_TO_USER_DATA); + } else { + ingredientManager = deserializeIngredientManager(getAbsolutePathFromResourceFolder(PATH_TO_DEFAULT_INGREDIENTS)); + } + + Path recipeFolder = Path.of(userPath + "/recipes"); + if (Files.exists(recipeFolder) && Files.isDirectory(recipeFolder)) { + recipeManager = deserializeRecipeManager(PATH_TO_USER_DATA + "/recipes", ingredientManager); + } else { + recipeFolder.toFile().mkdir(); + recipeManager = new RecipeManager(); + } + return null; + } } - public TastyPages deserializeFromFile(String filepath) { + 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 RecipeManager deserializeRecipeManager(String path, IngredientManager ingredientManager) throws Exception { + Path recipePath = Path.of(path); + List<Path> recipePaths; + try (Stream<Path> stream = Files.list(recipePath)) { //TODO: catch clause + 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 ""; + } + }) + .map(FileManager::JSONtoRecipe) + .toList(); + return new RecipeManager(recipes); + } + + private static CategoryManager deserializeCategoryManager() { return null; } - private void recipeToJSON() { + private static MealPlan deserializeMealPlan() { + return null; + } + + private static ShoppingList deserializeShoppingList() { + return null; + } + + private static String recipeToJSON(Recipe recipe, IngredientManager ingredientManager) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Recipe.class, new RecipeTypeAdapter(ingredientManager)) + .create(); + return gson.toJson(recipe); + } + + /** + * Used to convert an ingredient into a CSV String seperated by comma + * + * @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( + "%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) { + //when loading a recipe, is it important that its ingredients are the same objects as in the ingredient manager? + gson.fromJson(json, Recipe.class); + return null; } - private void ingredientToCSV() { + private static String getAbsolutePathFromResourceFolder(String resource) throws URISyntaxException { + URL resourceUrl = FileManager.class.getResource(resource); + assert resourceUrl != null; + Path path = Paths.get(resourceUrl.toURI()); + return path.toFile().getAbsolutePath(); } } diff --git a/src/main/java/mi/hdm/recipes/Category.java b/src/main/java/mi/hdm/recipes/Category.java index e654671..f5a7fd3 100644 --- a/src/main/java/mi/hdm/recipes/Category.java +++ b/src/main/java/mi/hdm/recipes/Category.java @@ -2,23 +2,30 @@ package mi.hdm.recipes; import mi.hdm.exceptions.InvalidCategoryException; -public class Category { +import java.util.Objects; +public class Category { + private final int code; private final String name; private final String colourCode; //0x für Hexzahlen - - public Category (String name, int colourCode) { - if(name == null || name.equals("")) { + public Category(String name, int colourCode) { + if (name == null || name.equals("")) { throw new InvalidCategoryException("Can not add category with name " + name); } - if(colourCode < 0 || colourCode > 0xFFFFFF) { + if (colourCode < 0 || colourCode > 0xFFFFFF) { throw new InvalidCategoryException("Can not add category with color code " + colourCode); } this.name = name; this.colourCode = CategoryManager.numberToColorCodeString(colourCode); + this.code = Objects.hash(name, colourCode); } + public Category(String name, String colourCode, int code) { + this.name = name; + this.colourCode = colourCode; + this.code = code; + } public String getColourCode() { return colourCode; @@ -41,4 +48,7 @@ public class Category { return String.format("Category %s with color code %s", name, colourCode); } + public int getCode() { + return code; + } } diff --git a/src/main/java/mi/hdm/recipes/CategoryManager.java b/src/main/java/mi/hdm/recipes/CategoryManager.java index 63fa28b..717a5f0 100644 --- a/src/main/java/mi/hdm/recipes/CategoryManager.java +++ b/src/main/java/mi/hdm/recipes/CategoryManager.java @@ -4,21 +4,27 @@ import mi.hdm.exceptions.InvalidCategoryException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; public class CategoryManager { private static final Logger log = LogManager.getLogger(CategoryManager.class); - private final List<Category> allCategories; + private final Map<Integer, Category> allCategories; //TODO: save in hash map with key being the code of each category public CategoryManager() { - allCategories = new ArrayList<>(); + allCategories = new HashMap<>(); + } + + public CategoryManager(Map<Integer, Category> categories) { + allCategories = new HashMap<>(categories); } public CategoryManager(List<Category> categories) { - allCategories = categories; + allCategories = new HashMap<>(); + categories.forEach(c -> allCategories.put(c.getCode(), c)); } /** @@ -30,18 +36,18 @@ public class CategoryManager { */ public void addCategory(String name, int colourCode) { Category c = new Category(name, colourCode); - if (getCategoryByName(name).isPresent() || getCategoryByCode(colourCode).isPresent()) { + if (getCategoryByName(name).isPresent() || getCategoryByColourCode(colourCode).isPresent()) { log.error("Category {} not added because it already exists", c.getName()); throw new InvalidCategoryException("Category already exists."); } else { - allCategories.add(c); + allCategories.put(c.getCode(), c); log.info("Category {} added successfully.", c.getName()); } } public void deleteCategory(Category c) { log.info("Category {} deleted successfully.", c.getName()); - if (!allCategories.remove(c)) { + if (!allCategories.values().remove(c)) { throw new InvalidCategoryException("Category is not listed."); } } @@ -52,7 +58,7 @@ public class CategoryManager { } public List<Category> getAllCategories() { - return allCategories; + return allCategories.values().stream().toList(); } public void clearCategories() { @@ -61,20 +67,32 @@ public class CategoryManager { } private Optional<Category> getCategoryByName(String name) { - return allCategories.stream() + return allCategories.values().stream() .filter(c -> c.getName().equals(name)) .findFirst(); } - private Optional<Category> getCategoryByCode(int colourCode) { - return allCategories.stream() + private Optional<Category> getCategoryByColourCode(int colourCode) { + return allCategories.values().stream() .filter(c -> c.getColourCode().equals(numberToColorCodeString(colourCode))) .findFirst(); } + public List<Category> getCategoriesFromKeys(List<Integer> keys) { + return keys + .stream() + .map(key -> { + Category c = allCategories.get(key); + if (c == null) + throw new InvalidCategoryException("Invalid category: no category exists with key " + key); + return c; + }) + .toList(); + } + public static String numberToColorCodeString(int colourCode) { String intToHex = Integer.toHexString(colourCode); intToHex = intToHex.indent(6 - intToHex.length()).replace(" ", "0"); - return String.format("#%s", intToHex); + return ("#" + intToHex).strip(); } } diff --git a/src/main/java/mi/hdm/recipes/Ingredient.java b/src/main/java/mi/hdm/recipes/Ingredient.java index d07cc2e..3b1bc19 100644 --- a/src/main/java/mi/hdm/recipes/Ingredient.java +++ b/src/main/java/mi/hdm/recipes/Ingredient.java @@ -2,7 +2,10 @@ package mi.hdm.recipes; import mi.hdm.exceptions.InvalidIngredientException; +import java.util.Objects; + public class Ingredient implements RecipeComponent { + private final String code; private final Measurement unit; private final String name; private final NutritionTable nutritionTable; @@ -18,12 +21,15 @@ public class Ingredient implements RecipeComponent { this.unit = unit; this.name = name; this.nutritionTable = nutritionTable; + this.code = calculateUniqueCode(); } + @Override public String getName() { return name; } + @Override public NutritionTable getNutritionTable() { return nutritionTable; } @@ -33,6 +39,16 @@ public class Ingredient implements RecipeComponent { return unit; } + @Override + public String getUniqueCode() { + return code; + } + + private String calculateUniqueCode() { + int hash = Objects.hash(name, nutritionTable.getTable()); + return "i" + hash; + } + @Override public boolean equals(Object o) { if (o instanceof Ingredient in) { diff --git a/src/main/java/mi/hdm/recipes/IngredientManager.java b/src/main/java/mi/hdm/recipes/IngredientManager.java index 909e84a..9ea3d57 100644 --- a/src/main/java/mi/hdm/recipes/IngredientManager.java +++ b/src/main/java/mi/hdm/recipes/IngredientManager.java @@ -4,22 +4,30 @@ import mi.hdm.exceptions.InvalidIngredientException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; + public class IngredientManager { private static final Logger log = LogManager.getLogger(IngredientManager.class); - private final List<Ingredient> allIngredients; + private final Map<String, Ingredient> allIngredients; //Map Key of ingredient to ingredient object - public IngredientManager() { - allIngredients = new ArrayList<>(); + public IngredientManager() { //TODO: save in hash map with key being the uniquely generated code + allIngredients = new HashMap<>(); } public IngredientManager(List<Ingredient> ingredients) { + allIngredients = new HashMap<>(); + ingredients.forEach(i -> allIngredients.put(i.getName(), i)); + } + + public IngredientManager(Map<String, Ingredient> ingredients) { allIngredients = ingredients; } + /** * Adds an ingredient if there is no equal ingredient (no ingredient with the same name). * @@ -32,7 +40,7 @@ public class IngredientManager { throw new InvalidIngredientException("Ingredient already exists."); } else { log.info("Ingredient {} added successfully.", in.getName()); - allIngredients.add(in); + allIngredients.put(in.getUniqueCode(), in); } } @@ -41,15 +49,10 @@ public class IngredientManager { addIngredient(in); } - public Ingredient deleteIngredient(int i) { - log.info("Ingredient {} deleted successfully.", allIngredients.get(i).getName()); - return allIngredients.remove(i); - } - public void deleteIngredient(String name) { Ingredient ingredient = getIngredientByName(name).orElseThrow(() -> new InvalidIngredientException("No ingredient with name " + name)); log.info("Ingredient {} deleted successfully.", name); - allIngredients.remove(ingredient); + allIngredients.values().remove(ingredient); } public void clearIngredients() { @@ -57,12 +60,28 @@ public class IngredientManager { allIngredients.clear(); } - public List<Ingredient> getAllIngredients() { + public Ingredient getIngredient(String code) { + Ingredient i = allIngredients.get(code); + if (i == null) { + throw new InvalidIngredientException("No ingredient with this code exists."); + } + return i; + } + + public Map<String, Ingredient> getAllIngredients() { return allIngredients; } + public Map<Ingredient, Integer> getIngredientsFromKeys(Map<String, Integer> m) { + Map<Ingredient, Integer> result = new HashMap<>(); + for (String key : m.keySet()) { + result.put(allIngredients.get(key), m.get(key)); + } + return result; + } + private Optional<Ingredient> getIngredientByName(String name) { - for (final Ingredient i : allIngredients) { + for (final Ingredient i : allIngredients.values()) { if (name.equals(i.getName())) { return Optional.of(i); } diff --git a/src/main/java/mi/hdm/recipes/NutritionCalculator.java b/src/main/java/mi/hdm/recipes/NutritionCalculator.java index 7e7a396..3897d96 100644 --- a/src/main/java/mi/hdm/recipes/NutritionCalculator.java +++ b/src/main/java/mi/hdm/recipes/NutritionCalculator.java @@ -9,7 +9,6 @@ import java.time.LocalDate; import java.util.Map; public class NutritionCalculator { - private static final Logger log = LogManager.getLogger(NutritionCalculator.class); /** @@ -31,7 +30,6 @@ public class NutritionCalculator { totalSalt = BigDecimal.ZERO; for (RecipeComponent entry : ingredients.keySet()) { - final Map<Nutrition, BigDecimal> nutritionTable = entry.getNutritionTable().getTable(); int divisor; diff --git a/src/main/java/mi/hdm/recipes/NutritionTable.java b/src/main/java/mi/hdm/recipes/NutritionTable.java index 226f32e..0ef2320 100644 --- a/src/main/java/mi/hdm/recipes/NutritionTable.java +++ b/src/main/java/mi/hdm/recipes/NutritionTable.java @@ -82,9 +82,15 @@ public class NutritionTable { @Override public String toString() { - return "NutritionTable{" + - "table=" + table + - '}'; + String nutritionTable = "Nutrition Table"; + + final StringBuilder stringBuilder = new StringBuilder(nutritionTable + "\n"); + stringBuilder.append("-".repeat(23)).append("\n"); + table.keySet() + .forEach(key -> stringBuilder.append(String.format("| %10s : %06.2f |%n", key, table.get(key).doubleValue()))); + stringBuilder.append("-".repeat(23)); + + return stringBuilder.toString(); } public static NutritionTable empty() { diff --git a/src/main/java/mi/hdm/recipes/Recipe.java b/src/main/java/mi/hdm/recipes/Recipe.java index aba7f4a..66898db 100644 --- a/src/main/java/mi/hdm/recipes/Recipe.java +++ b/src/main/java/mi/hdm/recipes/Recipe.java @@ -5,19 +5,17 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; public class Recipe implements RecipeComponent { private static final Logger log = LogManager.getLogger(Recipe.class); - private Map<RecipeComponent, Integer> ingredients; + private final String code; private String name; + private Map<String, Integer> ingredients; //TODO: save in hash map with key being the uniquely generated code private String description; private List<String> preparation; - private List<Category> categories; + private List<Integer> categories; //TODO: save in hash map with key being the hash code private int preparationTimeMins; private NutritionTable nutritionTable; private final LocalDateTime creationTime; @@ -40,7 +38,7 @@ public class Recipe implements RecipeComponent { Map<RecipeComponent, Integer> ingredients, String description, List<String> preparation, - List<Category> categories, + List<Integer> categories, int preparationTimeMins) { //Das ruft den anderen Konstruktor dieser Klasse auf (siehe unten) @@ -63,11 +61,11 @@ public class Recipe implements RecipeComponent { Map<RecipeComponent, Integer> ingredients, String description, List<String> preparation, - List<Category> categories, + List<Integer> categories, int preparationTimeMins, NutritionTable nutritionTable) { - setIngredients(ingredients); + setIngredientFromRecipeComponents(ingredients); setDescription(description); setPreparation(preparation); setNutritionTable(nutritionTable); @@ -76,6 +74,29 @@ public class Recipe implements RecipeComponent { setCategories(categories); this.creationTime = LocalDateTime.now(); + this.code = calculateUniqueCode(); + } + + public Recipe( + String name, + Map<String, Integer> ingredients, + String description, + List<String> preparation, + List<Integer> categories, + Integer preparationTimeMins, + NutritionTable nutritionTable, + LocalDateTime creationTime, + String code) { + + this.ingredients = ingredients; + setDescription(description); + setPreparation(preparation); + setNutritionTable(nutritionTable); + setName(name); + setPreparationTimeMins(preparationTimeMins); + setCategories(categories); + this.creationTime = creationTime; + this.code = code; } public void setName(String name) { @@ -111,21 +132,21 @@ public class Recipe implements RecipeComponent { log.info("Preparation set successfully."); } - public List<Category> getCategories() { + public List<Integer> getCategoryCodes() { return categories; } - public void setCategories(List<Category> categories) { + public void setCategories(List<Integer> categories) { if (categories == null) { categories = new ArrayList<>(); } - this.categories = categories; + this.categories = new ArrayList<>(categories); log.info("Categories set successfully."); } public void addCategory(Category category) { - categories.add(category); + categories.add(category.getCode()); log.info("Category {} added successfully.", category.getName()); } @@ -133,9 +154,9 @@ public class Recipe implements RecipeComponent { return preparationTimeMins; } - public void setPreparationTimeMins(int preparationTimeMins) { - if (preparationTimeMins < 0) { - throw new InvalidRecipeException("PreparationTime must be a positive value"); + public void setPreparationTimeMins(Integer preparationTimeMins) { + if (preparationTimeMins == null || preparationTimeMins < 0) { + throw new InvalidRecipeException("Preparation time must be a positive integer"); } this.preparationTimeMins = preparationTimeMins; } @@ -161,17 +182,32 @@ public class Recipe implements RecipeComponent { return Measurement.PIECE; } - public Map<RecipeComponent, Integer> getIngredients() { + public Map<String, Integer> getIngredients() { return ingredients; } - public void setIngredients(Map<RecipeComponent, Integer> ingredients) { + public void setIngredientFromRecipeComponents(Map<RecipeComponent, Integer> ingredients) { + if (ingredients == null || ingredients.size() == 0) { + throw new InvalidRecipeException("Ingredient list can not be null or empty"); + } + this.ingredients = recipeObjectMapToKeyMap(ingredients); + } + + public void setIngredientFromKeys(Map<String, Integer> ingredients) { if (ingredients == null || ingredients.size() == 0) { throw new InvalidRecipeException("Ingredient list can not be null or empty"); } this.ingredients = ingredients; } + private Map<String, Integer> recipeObjectMapToKeyMap(Map<RecipeComponent, Integer> map) { + Map<String, Integer> result = new HashMap<>(); + for (RecipeComponent c : map.keySet()) { + result.put(c.getUniqueCode(), map.get(c)); + } + return result; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -185,4 +221,14 @@ public class Recipe implements RecipeComponent { String desc = description == null ? "No description" : description; return String.format("Recipe: %s%n--------%n%s%n%nCreated: %s%n", name, desc, creationTime); } + + @Override + public String getUniqueCode() { + return code; + } + + private String calculateUniqueCode() { + int hash = Objects.hash(name, creationTime); + return "r" + hash; + } } diff --git a/src/main/java/mi/hdm/recipes/RecipeComponent.java b/src/main/java/mi/hdm/recipes/RecipeComponent.java index 0e14742..bd85f88 100644 --- a/src/main/java/mi/hdm/recipes/RecipeComponent.java +++ b/src/main/java/mi/hdm/recipes/RecipeComponent.java @@ -7,4 +7,11 @@ public interface RecipeComponent { NutritionTable getNutritionTable(); Measurement getMeasurement(); + + /** + * The code is generated using by hashing fields of the object. It will be generated on object creation and never change. + * + * @return Recipes will return "r"+hash, ingredients will return "i"+hash + */ + String getUniqueCode(); } diff --git a/src/main/java/mi/hdm/recipes/RecipeManager.java b/src/main/java/mi/hdm/recipes/RecipeManager.java index 1ec7307..6149726 100644 --- a/src/main/java/mi/hdm/recipes/RecipeManager.java +++ b/src/main/java/mi/hdm/recipes/RecipeManager.java @@ -4,28 +4,24 @@ import mi.hdm.exceptions.InvalidRecipeException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; public class RecipeManager { //TODO observable array list? private static final Logger log = LogManager.getLogger(RecipeManager.class); - private final List<Recipe> recipes; - private final CategoryManager categories; - private final IngredientManager ingredients; + private final Map<String, Recipe> allRecipes; - public RecipeManager(List<Recipe> recipes, CategoryManager categories, IngredientManager ingredients) { - this.recipes = recipes; - this.categories = categories; - this.ingredients = ingredients; + public RecipeManager(List<Recipe> recipes) { + this.allRecipes = new HashMap<>(); + recipes.forEach(i -> allRecipes.put(i.getName(), i)); } public RecipeManager() { - recipes = new ArrayList<>(); - categories = new CategoryManager(); - ingredients = new IngredientManager(); + allRecipes = new HashMap<>(); } public void addRecipe(Recipe recipe) { @@ -33,32 +29,31 @@ public class RecipeManager { log.error("Recipe '{}' not added because another recipe with the same name already exists", recipe.getName()); throw new InvalidRecipeException("Recipe with this name already exists."); } - recipes.add(recipe); + allRecipes.put(recipe.getUniqueCode(), recipe); log.info("Recipe {} added successfully.", recipe.getName()); } - public Recipe deleteRecipe(int i) { - log.info("Recipe {} deleted successfully.", recipes.get(i).getName()); - return recipes.remove(i); + public Recipe getRecipeByCode(String code) { + return allRecipes.get(code); } public void deleteRecipe(String name) { Recipe r = getRecipeByName(name).orElseThrow(() -> new InvalidRecipeException("A recipe with this name does not exist")); log.info("Recipe {} deleted successfully.", name); - recipes.remove(r); + allRecipes.values().remove(r); } // Exception vs return false: What's the best approach? // -> false for normal situations, exceptions for real faults which should not occur public void deleteRecipe(Recipe r) { - if (!recipes.remove(r)) { + if (!allRecipes.values().remove(r)) { throw new InvalidRecipeException("Recipe is not listed."); } log.info("Recipe deleted successfully."); } private Optional<Recipe> getRecipeByName(String name) { - for (final Recipe r : recipes) { + for (final Recipe r : allRecipes.values()) { if (name.equals(r.getName())) { return Optional.of(r); } @@ -67,6 +62,6 @@ public class RecipeManager { } public List<Recipe> getRecipes() { - return recipes; + return allRecipes.values().stream().toList(); } } diff --git a/src/main/java/mi/hdm/recipes/RecipeSearch.java b/src/main/java/mi/hdm/recipes/RecipeSearch.java index 4438984..59f6bc3 100644 --- a/src/main/java/mi/hdm/recipes/RecipeSearch.java +++ b/src/main/java/mi/hdm/recipes/RecipeSearch.java @@ -8,10 +8,16 @@ import java.util.List; public class RecipeSearch { //TODO: lets make these methods static, no need to instantiate a new object every time - private final List<Recipe> recipesToSearch; private static final Logger log = LogManager.getLogger(RecipeSearch.class); - public RecipeSearch (List<Recipe> recipesToSearch) { + private final RecipeManager recipeManager; + private final IngredientManager ingredientManager; + + private final List<Recipe> recipesToSearch; + + public RecipeSearch(RecipeManager recipeManager, IngredientManager ingredientManager, List<Recipe> recipesToSearch) { + this.recipeManager = recipeManager; + this.ingredientManager = ingredientManager; this.recipesToSearch = recipesToSearch; } @@ -36,9 +42,13 @@ public class RecipeSearch { if (r.getName().toLowerCase().contains(l)) { //check if recipe name contains words from query (convert name of recipe to lowercase too) result.add(r); } - for (final RecipeComponent c : r.getIngredients().keySet()) { - if (c.getName().toLowerCase().contains(l)) { //check if ingredients contain words from query - result.add(r); + for (final String key : r.getIngredients().keySet()) { + if (key.charAt(0) == 'i') { + Ingredient i = ingredientManager.getIngredient(key); + if (i.getName().toLowerCase().contains(l)) result.add(r); + } else { + Recipe recipe = recipeManager.getRecipeByCode(key); + if (recipe.getName().toLowerCase().contains(l)) result.add(r); } } } @@ -59,7 +69,7 @@ public class RecipeSearch { for (final Recipe r : recipesToSearch) { for (final Category c : categoriesToSearch) { - if (!r.getCategories().contains(c)) { //if a recipe does not contain one of the categories it will be removed from the result set + if (!r.getCategoryCodes().contains(c)) { //if a recipe does not contain one of the categories it will be removed from the result set result.remove(r); } } diff --git a/src/main/java/mi/hdm/shoppingList/ShoppingList.java b/src/main/java/mi/hdm/shoppingList/ShoppingList.java index 340f9e3..34dc229 100644 --- a/src/main/java/mi/hdm/shoppingList/ShoppingList.java +++ b/src/main/java/mi/hdm/shoppingList/ShoppingList.java @@ -8,7 +8,6 @@ import org.apache.logging.log4j.Logger; import java.util.HashMap; import java.util.Map; -import java.util.Optional; /** * Shopping list that the user can add ingredients to. The elements on the list can be marked as done or undone. @@ -16,13 +15,13 @@ import java.util.Optional; public class ShoppingList { private static final Logger log = LogManager.getLogger(ShoppingList.class); - private final Map<Ingredient, Boolean> list; + private final Map<String, Boolean> list; public ShoppingList() { list = new HashMap<>(); } - public ShoppingList(Map<Ingredient, Boolean> shoppingListMap) { + public ShoppingList(Map<String, Boolean> shoppingListMap) { list = shoppingListMap; } @@ -35,18 +34,26 @@ public class ShoppingList { if (ingredient == null) { throw new InvalidIngredientException("Can not add ingredient that is null"); } - list.put(ingredient, false); + list.put(ingredient.getUniqueCode(), false); log.info("Added {} to shopping list.", ingredient.getName()); } + public void addToShoppingList(String key) { + if (key == null || key.charAt(0) != 'i') { + throw new InvalidIngredientException("Can not add ingredient for this key: " + key); + } + list.put(key, false); + log.info("Added ingredient with code {} to shopping list.", key); + } + public void addAllToShoppingList(Recipe recipe) { recipe.getIngredients().keySet() .forEach(element -> { - if (element instanceof Ingredient i) - addToShoppingList(i); - //adds recipes recursively (see below); comment next 2 lines out to avoid adding recipes recursively - else - addAllToShoppingList((Recipe) element); + if (element.startsWith("i")) + addToShoppingList(element); + //adds recipes recursively (see below); comment next 2 lines out to avoid adding recipes recursively + /*else + addAllToShoppingList((Recipe) element);*/ //TODO: needs recipe manager here } ); } @@ -54,17 +61,23 @@ public class ShoppingList { public void flipStatus(Ingredient in) { if (in == null) throw new NullPointerException("Ingredient can not be null"); - if (!list.containsKey(in)) + if (!list.containsKey(in.getUniqueCode())) throw new InvalidIngredientException("No ingredient with name " + in.getName()); - boolean newStatus = !list.get(in); - list.put(in, newStatus); + boolean newStatus = !list.get(in.getUniqueCode()); + list.put(in.getUniqueCode(), newStatus); log.info("Ingredient {} marked as {}.", in.getName(), newStatus ? "done" : "not done"); } - public void flipStatus(String name) { - Ingredient in = getIngredientByName(name).orElseThrow(() -> new InvalidIngredientException("No ingredient with name " + name)); - flipStatus(in); + public void flipStatus(String key) { + if (key == null) + throw new NullPointerException("Ingredient can not be null"); + if (!list.containsKey(key)) + throw new InvalidIngredientException("No ingredient with key " + key); + + boolean newStatus = !list.get(key); + list.put(key, newStatus); + log.info("Ingredient with key {} marked as {}.", key, newStatus ? "done" : "not done"); } /** @@ -72,26 +85,15 @@ public class ShoppingList { */ public void removeAllBought() { int removed = 0; - for (Ingredient i : list.keySet()) { - if (list.get(i)) list.remove(i); + for (String s : list.keySet()) { + if (list.get(s)) list.remove(s); removed++; - log.debug("Removed ingredient '{}' from shopping list.", i.getName()); + log.debug("Removed ingredient '{}' from shopping list.", s); } log.info("Removed {} ingredients from shopping list.", removed); } - public Map<Ingredient, Boolean> getList() { + public Map<String, Boolean> getList() { return list; } - - private Optional<Ingredient> getIngredientByName(String name) { - if (name == null) throw new NullPointerException("Name can not be null"); - - for (final Ingredient i : list.keySet()) { - if (name.equals(i.getName())) { - return Optional.of(i); - } - } - return Optional.empty(); - } } \ No newline at end of file diff --git a/src/main/java/mi/hdm/typeAdapters/CategoryTypeAdapter.java b/src/main/java/mi/hdm/typeAdapters/CategoryTypeAdapter.java new file mode 100644 index 0000000..a2825a8 --- /dev/null +++ b/src/main/java/mi/hdm/typeAdapters/CategoryTypeAdapter.java @@ -0,0 +1,53 @@ +package mi.hdm.typeAdapters; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import mi.hdm.recipes.Category; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +public class CategoryTypeAdapter extends TypeAdapter<Category> { + private final static Logger log = LogManager.getLogger(CategoryTypeAdapter.class); + + @Override + public void write(JsonWriter jsonWriter, Category category) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("name").value(category.getName()); + jsonWriter.name("colourCode").value(category.getColourCode()); + jsonWriter.name("code").value(category.getCode()); + jsonWriter.endObject(); + } + + @Override + public Category read(JsonReader jsonReader) throws IOException { + String name = null; + String colourCode = null; + Integer code = null; + + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String propertyName = jsonReader.nextName(); + if (propertyName.equals("name")) { + name = jsonReader.nextString(); + } else if (propertyName.equals("colourCode")) { + colourCode = jsonReader.nextString(); + } else if (propertyName.equals("code")) { + code = jsonReader.nextInt(); + } else { + jsonReader.skipValue(); + } + } + jsonReader.endObject(); + + if (name == null || colourCode == null || code == null) { + String msg = "Null value: invalid or missing value in JSON object for a category"; + log.error(msg); + throw new IOException(msg); + } + + return new Category(name, colourCode, code); + } +} diff --git a/src/main/java/mi/hdm/typeAdapters/NutritionTableTypeAdapter.java b/src/main/java/mi/hdm/typeAdapters/NutritionTableTypeAdapter.java new file mode 100644 index 0000000..beb5743 --- /dev/null +++ b/src/main/java/mi/hdm/typeAdapters/NutritionTableTypeAdapter.java @@ -0,0 +1,52 @@ +package mi.hdm.typeAdapters; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import mi.hdm.recipes.NutritionTable; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +public class NutritionTableTypeAdapter extends TypeAdapter<NutritionTable> { + private final static Logger log = LogManager.getLogger(NutritionTableTypeAdapter.class); + + @Override + public void write(JsonWriter jsonWriter, NutritionTable nutritionTable) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("values").value(new Gson().toJson(nutritionTable.getTable())); + jsonWriter.endObject(); + } + + @Override + public NutritionTable read(JsonReader jsonReader) throws IOException { + Double calories = null, carbs = null, fat = null, proteins = null, fibers = null, salt = null; + jsonReader.beginObject(); + if (jsonReader.hasNext()) { + String propertyName = jsonReader.nextName(); + if (propertyName.equals("values")) { + String valuesJson = jsonReader.nextString(); + JsonObject valuesObject = JsonParser.parseString(valuesJson).getAsJsonObject(); + calories = valuesObject.get("CALORIES").getAsDouble(); + carbs = valuesObject.get("CARBS").getAsDouble(); + fat = valuesObject.get("FAT").getAsDouble(); + proteins = valuesObject.get("PROTEINS").getAsDouble(); + fibers = valuesObject.get("FIBERS").getAsDouble(); + salt = valuesObject.get("SALT").getAsDouble(); + } else { + jsonReader.skipValue(); + } + } + jsonReader.endObject(); + try { + return new NutritionTable(calories, carbs, fat, proteins, fibers, salt); + } catch (NullPointerException e) { + log.error("IO Exception: invalid or missing values in JSON object for ingredient."); + throw new IOException("null pointer exception - invalid or missing values in JSON"); + } + } +} diff --git a/src/main/java/mi/hdm/typeAdapters/RecipeTypeAdapter.java b/src/main/java/mi/hdm/typeAdapters/RecipeTypeAdapter.java new file mode 100644 index 0000000..5540133 --- /dev/null +++ b/src/main/java/mi/hdm/typeAdapters/RecipeTypeAdapter.java @@ -0,0 +1,107 @@ +package mi.hdm.typeAdapters; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import mi.hdm.recipes.IngredientManager; +import mi.hdm.recipes.NutritionTable; +import mi.hdm.recipes.Recipe; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("DuplicatedCode") +public class RecipeTypeAdapter extends TypeAdapter<Recipe> { + private final static Logger log = LogManager.getLogger(RecipeTypeAdapter.class); + + private final IngredientManager ingredientManager; + + public RecipeTypeAdapter(IngredientManager ingredientManager) { + this.ingredientManager = ingredientManager; + } + + @Override + public void write(JsonWriter writer, Recipe recipe) throws IOException { + writer.beginObject(); + writer.name("name").value(recipe.getName()); + writer.name("nutritionTable").jsonValue(new GsonBuilder().registerTypeAdapter(NutritionTable.class, new NutritionTableTypeAdapter()).create().toJson(recipe.getNutritionTable())); + writer.name("code").value(recipe.getUniqueCode()); + + writer.name("ingredients").jsonValue(new Gson().toJson(recipe.getIngredients())); + + writer.name("description").value(recipe.getDescription()); + writer.name("preparation").jsonValue(new Gson().toJson(recipe.getPreparation())); + + writer.name("categories").jsonValue(new Gson().toJson(recipe.getCategoryCodes())); + + writer.name("preparationTimeMins").value(recipe.getPreparationTimeMins()); + writer.name("creationTime").value(recipe.getCreationTime().toString()); + writer.endObject(); + log.debug("Wrote recipe '{}' to JSON.", recipe.getName()); + } + + @Override + public Recipe read(JsonReader reader) throws IOException { + String name = null; + Map<String, Integer> ingredients = null; + String description = null; + List<String> preparation = null; + List<Integer> categories = null; + Integer preparationTimeMins = null; + NutritionTable nutritionTable = null; + LocalDateTime creationTime = null; + String code = null; + + reader.beginObject(); + while (reader.hasNext()) { + String propertyName = reader.nextName(); + switch (propertyName) { + case "name": + name = reader.nextString(); + break; + case "ingredients": + ingredients = new Gson().fromJson(reader, new TypeToken<Map<String, Integer>>() { + }.getType()); + break; + case "description": + description = reader.nextString(); + break; + case "preparation": + preparation = new Gson().fromJson(reader, new TypeToken<List<String>>() { + }.getType()); + break; + case "categories": + categories = new Gson().fromJson(reader, new TypeToken<List<Integer>>() { + }.getType()); + break; + case "preparationTimeMins": + preparationTimeMins = reader.nextInt(); + break; + case "nutritionTable": + nutritionTable = new GsonBuilder().registerTypeAdapter(NutritionTable.class, new NutritionTableTypeAdapter()).create().fromJson(reader, NutritionTable.class); + break; + case "creationTime": + creationTime = LocalDateTime.parse(reader.nextString()); + break; + case "code": + code = reader.nextString(); + break; + default: + reader.skipValue(); + break; + } + } + reader.endObject(); + + log.debug("Read recipe '{}' from JSON.", name); + + return new Recipe(name, ingredients, description, preparation, categories, preparationTimeMins, nutritionTable, creationTime, code); + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4dd3f6d..ca7b904 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -2,8 +2,9 @@ module gui { requires javafx.controls; requires javafx.fxml; requires org.apache.logging.log4j; + requires com.google.gson; - opens mi.hdm to javafx.fxml; + opens mi.hdm to javafx.fxml, com.google.gson; opens mi.hdm.controllers to javafx.fxml; exports mi.hdm; diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 5e7e35c..b75fabd 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -14,7 +14,7 @@ <Logger name="mi.hdm.GuiDriver" level="info"> <AppenderRef ref="A1"/> </Logger> - <Root level="info"> + <Root level="debug"> <AppenderRef ref="STDOUT"/> </Root> </Loggers> diff --git a/src/test/java/mi/hdm/filesystem/JsonTest.java b/src/test/java/mi/hdm/filesystem/JsonTest.java new file mode 100644 index 0000000..e7f26b9 --- /dev/null +++ b/src/test/java/mi/hdm/filesystem/JsonTest.java @@ -0,0 +1,61 @@ +package mi.hdm.filesystem; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import mi.hdm.TastyPages; +import mi.hdm.recipes.*; +import mi.hdm.typeAdapters.CategoryTypeAdapter; +import mi.hdm.typeAdapters.NutritionTableTypeAdapter; +import mi.hdm.typeAdapters.RecipeTypeAdapter; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JsonTest { + private final static IngredientManager ingredientManager = TastyPages.ingredientManager; + + @BeforeAll + public static void setupAll() { + ingredientManager.addIngredient(ValidObjectsPool.getValidIngredientTwo()); + ingredientManager.addIngredient(ValidObjectsPool.getValidIngredientOne()); + } + + @Test + public void testCategoryToJSON() { + Category expected = ValidObjectsPool.getValidCategoryOne(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(Category.class, new CategoryTypeAdapter()) + .create(); + + String jsonResult = gson.toJson(expected); + + Category result = gson.fromJson(jsonResult, Category.class); + assertEquals(expected, result); + } + + @Test + public void testNutritionTableToJSON() { + NutritionTable expected = ValidObjectsPool.getValidNutritionTableOne(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(NutritionTable.class, new NutritionTableTypeAdapter()) + .create(); + + String json = gson.toJson(expected, NutritionTable.class); + + NutritionTable result = gson.fromJson(json, NutritionTable.class); + assertEquals(expected, result); + } + + @Test + public void testRecipeToJSON() { + Recipe expected = ValidObjectsPool.getValidRecipeOne(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(Recipe.class, new RecipeTypeAdapter(ingredientManager)) + .create(); + String json = gson.toJson(expected); + System.out.println(json); + Recipe result = gson.fromJson(json, Recipe.class); + assertEquals(expected, result); + } +} diff --git a/src/test/java/mi/hdm/recipes/RecipeSearchTest.java b/src/test/java/mi/hdm/recipes/RecipeSearchTest.java index f29d421..034529e 100644 --- a/src/test/java/mi/hdm/recipes/RecipeSearchTest.java +++ b/src/test/java/mi/hdm/recipes/RecipeSearchTest.java @@ -1,5 +1,7 @@ package mi.hdm.recipes; +import mi.hdm.TastyPages; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -17,13 +19,24 @@ class RecipeSearchTest { private final static Category c1 = ValidObjectsPool.getValidCategoryOne(); private final static Category c2 = ValidObjectsPool.getValidCategoryTwo(); + private static final RecipeManager recipeManager = TastyPages.recipeManager; + private static final IngredientManager ingredientManager = TastyPages.ingredientManager; + private List<Recipe> recipes; private RecipeSearch underTest; + @BeforeAll + public static void setupAll() { + ingredientManager.addIngredient(ValidObjectsPool.getValidIngredientOne()); + ingredientManager.addIngredient(ValidObjectsPool.getValidIngredientTwo()); + recipeManager.addRecipe(r1); + recipeManager.addRecipe(r2); + } + @BeforeEach public void setup() { recipes = List.of(r1, r2); - underTest = new RecipeSearch(recipes); + underTest = new RecipeSearch(recipeManager, ingredientManager, recipes); } @ParameterizedTest diff --git a/src/test/java/mi/hdm/recipes/RecipeTest.java b/src/test/java/mi/hdm/recipes/RecipeTest.java index 8fdc169..913e702 100644 --- a/src/test/java/mi/hdm/recipes/RecipeTest.java +++ b/src/test/java/mi/hdm/recipes/RecipeTest.java @@ -88,7 +88,7 @@ class RecipeTest { //then assertEquals( List.of(category), - underTest.getCategories() + underTest.getCategoryCodes() ); } @@ -106,7 +106,7 @@ class RecipeTest { //then assertEquals( List.of(category), - underTest.getCategories() + underTest.getCategoryCodes() ); } } diff --git a/src/test/java/mi/hdm/recipes/ValidObjectsPool.java b/src/test/java/mi/hdm/recipes/ValidObjectsPool.java index e9edac9..aaf8ac3 100644 --- a/src/test/java/mi/hdm/recipes/ValidObjectsPool.java +++ b/src/test/java/mi/hdm/recipes/ValidObjectsPool.java @@ -3,7 +3,6 @@ package mi.hdm.recipes; import mi.hdm.mealPlan.MealPlan; import java.time.LocalDate; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -14,8 +13,8 @@ public class ValidObjectsPool { private final static NutritionTable nutritionTableTwo = new NutritionTable(263.1, 25.2, 0.2, 0.0, 0.0, 0.8); private final static Ingredient ingredientOne = new Ingredient(Measurement.GRAM, "Zucker", nutritionTableOne); private final static Ingredient ingredientTwo = new Ingredient(Measurement.GRAM, "Apple", nutritionTableTwo); - private final static Recipe recipeOne = new Recipe("Valid recipe 1", Map.of(ingredientOne, 100, ingredientTwo, 200), "Description for this recipe", List.of("step one", "step two"), List.of(categoryOne), 15); - private final static Recipe recipeTwo = new Recipe("Apfelkuchen", Map.of(ingredientOne, 250), "Mein liebster APfelkuchen", List.of("mjam mjam", "ich liebe kochen"), List.of(categoryTwo, categoryOne), 25); + private final static Recipe recipeOne = new Recipe("Valid recipe 1", Map.of(ingredientOne, 100, ingredientTwo, 200), "Description for this recipe", List.of("step one", "step two"), List.of(categoryOne.getCode()), 15); + private final static Recipe recipeTwo = new Recipe("Apfelkuchen", Map.of(ingredientOne, 250), "Mein liebster APfelkuchen", List.of("mjam mjam", "ich liebe kochen"), List.of(categoryTwo.getCode(), categoryOne.getCode()), 25); private final static Map<LocalDate, Recipe> recipeMap1 = Map.of(LocalDate.now(), recipeOne, LocalDate.now().plusDays(1), recipeTwo); private final static MealPlan planOne = new MealPlan(recipeMap1); diff --git a/src/test/java/mi/hdm/shoppingList/ShoppingListTest.java b/src/test/java/mi/hdm/shoppingList/ShoppingListTest.java index ec06b37..a44f3b3 100644 --- a/src/test/java/mi/hdm/shoppingList/ShoppingListTest.java +++ b/src/test/java/mi/hdm/shoppingList/ShoppingListTest.java @@ -27,7 +27,7 @@ class ShoppingListTest { @Test public void shouldAddIngredient() { //given - Map<Ingredient, Boolean> expected = Map.of(ing1, false); + Map<String, Boolean> expected = Map.of(ing1.getUniqueCode(), false); //when underTest.addToShoppingList(ing1); @@ -39,7 +39,7 @@ class ShoppingListTest { @Test public void shouldAddAllIngredients() { //given - Map<Ingredient, Boolean> expected = Map.of(ing1, false, ing2, false); + Map<String, Boolean> expected = Map.of(ing1.getUniqueCode(), false, ing2.getUniqueCode(), false); //when underTest.addAllToShoppingList(r1); @@ -51,7 +51,7 @@ class ShoppingListTest { @Test public void testSetStatusByObject() { //given - Map<Ingredient, Boolean> expected = Map.of(ing1, true); + Map<String, Boolean> expected = Map.of(ing1.getUniqueCode(), true); underTest.addToShoppingList(ing1); //when @@ -64,24 +64,24 @@ class ShoppingListTest { } @Test - public void testSetStatusByName() { + public void testSetStatusByKey() { //given - Map<Ingredient, Boolean> expected = Map.of(ing1, true); + Map<String, Boolean> expected = Map.of(ing1.getUniqueCode(), true); underTest.addToShoppingList(ing1); //when - underTest.flipStatus(ing1.getName()); + underTest.flipStatus(ing1.getUniqueCode()); //expect assertEquals(expected, underTest.getList()); - assertThrows(InvalidIngredientException.class, () -> underTest.flipStatus(ing2.getName())); - assertThrows(NullPointerException.class, () -> underTest.flipStatus((String) null)); + assertThrows(InvalidIngredientException.class, () -> underTest.flipStatus(ing2)); + assertThrows(NullPointerException.class, () -> underTest.flipStatus((Ingredient) null)); } @Test public void shouldRemoveAllBoughtItems() { //given - Map<Ingredient, Boolean> expected = Map.of(); + Map<String, Boolean> expected = Map.of(); underTest.addToShoppingList(ing1); underTest.flipStatus(ing1); -- GitLab