diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index 56f6c6e..37541cb 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -11,7 +11,7 @@ import client.scenes.recipe.RecipeDetailCtrl; import client.utils.Config; import client.utils.ConfigService; -import client.utils.DefaultRecipeFactory; +import client.utils.DefaultValueFactory; import client.utils.LocaleAware; import client.utils.LocaleManager; import client.utils.ServerUtils; @@ -217,7 +217,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { */ @FXML private void addRecipe() { - Recipe newRecipe = DefaultRecipeFactory.getDefaultRecipe(); // Create default recipe + Recipe newRecipe = DefaultValueFactory.getDefaultRecipe(); // Create default recipe try { newRecipe = server.addRecipe(newRecipe); // get the new recipe id refresh(); // refresh view with server recipes diff --git a/client/src/main/java/client/scenes/nutrition/NutritionViewCtrl.java b/client/src/main/java/client/scenes/nutrition/NutritionViewCtrl.java index 493db8e..7cac4e1 100644 --- a/client/src/main/java/client/scenes/nutrition/NutritionViewCtrl.java +++ b/client/src/main/java/client/scenes/nutrition/NutritionViewCtrl.java @@ -2,6 +2,7 @@ package client.scenes.nutrition; import client.scenes.FoodpalApplicationCtrl; import com.google.inject.Inject; +import commons.Ingredient; import commons.Recipe; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -14,12 +15,12 @@ import java.util.Set; public class NutritionViewCtrl { private ObservableList recipes; - private HashMap ingredientStats; - public ListView nutritionIngredientsView; + private HashMap ingredientStats; + public ListView nutritionIngredientsView; private final NutritionDetailsCtrl nutritionDetailsCtrl; // TODO into Ingredient class definition - + // FIXME MOST LIKELY CURRENTLY BROKEN. TO BE FIXED. /** * Comedically verbose function to count unique appearances of an ingredient by name in each recipe. * For each recipe: @@ -29,13 +30,15 @@ public class NutritionViewCtrl { * 2. For each recipe in list: * 1. If the name of the ingredient exists in the recipe list, increment the statistic by 1. * 2. Else maintain the same value for that statistic. - * @param recipeList + * @param recipeList The recipe list */ private void updateIngredientStats( List recipeList ) { recipeList.forEach(recipe -> { - Set uniqueIngredients = new HashSet<>(recipe.getIngredients()); + Set uniqueIngredients = new HashSet<>( + recipe.getIngredients().stream().map( + ingredient -> ingredient.ingredient).toList()); nutritionIngredientsView.getItems().setAll(uniqueIngredients); uniqueIngredients.forEach(ingredient -> { ingredientStats.put(ingredient, 0); diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCell.java b/client/src/main/java/client/scenes/recipe/IngredientListCell.java index d712302..c4c0d25 100644 --- a/client/src/main/java/client/scenes/recipe/IngredientListCell.java +++ b/client/src/main/java/client/scenes/recipe/IngredientListCell.java @@ -1,32 +1,71 @@ package client.scenes.recipe; +import commons.FormalIngredient; +import commons.Ingredient; +import commons.RecipeIngredient; import commons.Unit; +import commons.VagueIngredient; import javafx.collections.FXCollections; import javafx.scene.control.ChoiceBox; import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; import javafx.scene.layout.HBox; +import java.util.Optional; + /** - * A custom {@link OrderedEditableListCell} for displaying and editing ingredients in an - * IngredientList. Allows inline editing of ingredient names. + * A custom {@link OrderedEditableListCell} for + * displaying and editing ingredients in an IngredientList. + * Allows inline editing of ingredient names. * * @see IngredientListCtrl */ -public class IngredientListCell extends OrderedEditableListCell { +public class IngredientListCell extends OrderedEditableListCell { @Override - public void commitEdit(String newValue) { + public void commitEdit(RecipeIngredient newValue) { super.commitEdit(newValue); } + /** + * Starts an edit box on the ingredient. + * It needs an {@link HBox HBox} container to fit 3 input boxes. + * @see OrderedEditableListCell + */ @Override public void startEdit() { super.startEdit(); - TextField amountInput = new TextField("0"); + RecipeIngredient ingredient = getItem(); + + Optional templateAmount = Optional.empty(); + Optional unit = Optional.empty(); + + // Initialize the input fields with some initial data if we get a formal ingredient + if (ingredient.getClass().equals(FormalIngredient.class)) { + FormalIngredient formal = (FormalIngredient) ingredient; + templateAmount = Optional.of(formal.getAmount()); + unit = Optional.of(Unit.fromString(formal.getUnitSuffix()).orElseThrow( + () -> new RuntimeException("FormalIngredient whereas invalid unit"))); + } + // TODO initialize some other data in the case of vague ingredient + + // Initialize the input boxes + TextField amountInput = new TextField(templateAmount.map(Object::toString).orElse(null)); ChoiceBox unitChoice = new ChoiceBox<>(); - unitChoice.setItems(FXCollections.observableArrayList(Unit.GRAM, Unit.KILOGRAM)); - TextField nameInput = new TextField("ingredient"); + + // initialize the current unit if it is present + unit.ifPresent(unitChoice::setValue); + unitChoice.setItems(FXCollections.observableArrayList(Unit.values())); + + // calls makeInputLine to create the inline edit container + HBox container = makeInputLine(ingredient, amountInput, unitChoice); + + // set the graphic to the edit box + this.setText(null); + this.setGraphic(container); + } + private HBox makeInputLine(RecipeIngredient ingredient, TextField amountInput, ChoiceBox unitChoice) { + TextField nameInput = new TextField(ingredient.ingredient.name); HBox container = new HBox(amountInput, unitChoice, nameInput); container.setOnKeyReleased(event -> { if (event.getCode() != KeyCode.ENTER) { @@ -34,15 +73,16 @@ public class IngredientListCell extends OrderedEditableListCell { } Unit unit = unitChoice.getValue(); String name = nameInput.getText(); - if (unit == null) { + if (unit == null || !unit.isFormal()) { String desc = amountInput.getText(); - commitEdit(desc + " " + name); + Ingredient newIngredient = new Ingredient(name, 0., 0., 0.); + commitEdit(new VagueIngredient(newIngredient, desc)); return; } - int amount = Integer.parseInt(amountInput.getText()); - commitEdit(amount + " " + unit + " of " + name); + double amount = Double.parseDouble(amountInput.getText()); + Ingredient newIngredient = new Ingredient(name, 0., 0., 0.); + commitEdit(new FormalIngredient(newIngredient, amount, unit.suffix)); }); - this.setText(null); - this.setGraphic(container); + return container; } } diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java index 859c28a..7b6bdff 100644 --- a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java +++ b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java @@ -1,12 +1,16 @@ package client.scenes.recipe; +import client.utils.DefaultValueFactory; import client.utils.LocaleAware; import client.utils.LocaleManager; import com.google.inject.Inject; +import commons.FormalIngredient; import commons.Recipe; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; + +import commons.RecipeIngredient; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; @@ -40,15 +44,15 @@ public class IngredientListCtrl implements LocaleAware { * changes and update the recipe accordingly. *

*/ - private ObservableList ingredients; + private ObservableList ingredients; /** * A callback function that is called when the ingredient list is updated. */ - private Consumer> updateCallback; + private Consumer> updateCallback; @FXML - public ListView ingredientListView; + public ListView ingredientListView; @FXML public Label ingredientsLabel; @@ -69,7 +73,7 @@ public class IngredientListCtrl implements LocaleAware { if (recipe == null) { this.ingredients = FXCollections.observableArrayList(new ArrayList<>()); } else { - List ingredientList = recipe.getIngredients(); + List ingredientList = recipe.getIngredients(); this.ingredients = FXCollections.observableArrayList(ingredientList); } @@ -82,7 +86,7 @@ public class IngredientListCtrl implements LocaleAware { * * @param callback The function to call upon each update. */ - public void setUpdateCallback(Consumer> callback) { + public void setUpdateCallback(Consumer> callback) { this.updateCallback = callback; } @@ -99,12 +103,13 @@ public class IngredientListCtrl implements LocaleAware { * @param event The action event. */ private void handleIngredientAdd(ActionEvent event) { - this.ingredients.add("Ingredient " + (this.ingredients.size() + 1)); + FormalIngredient newIngredient = DefaultValueFactory.getDefaultFormalIngredient(); + this.ingredients.add(newIngredient); this.refresh(); this.updateCallback.accept(this.ingredients); var select = this.ingredientListView.getSelectionModel(); - select.select(this.ingredients.size() - 1); + select.select(newIngredient); } /** @@ -112,9 +117,9 @@ public class IngredientListCtrl implements LocaleAware { * * @param event The edit event. */ - private void handleIngredientEdit(EditEvent event) { + private void handleIngredientEdit(EditEvent event) { int index = event.getIndex(); - String newValue = event.getNewValue(); + RecipeIngredient newValue = event.getNewValue(); this.ingredients.set(index, newValue); this.refresh(); @@ -154,9 +159,6 @@ public class IngredientListCtrl implements LocaleAware { @Override public void initializeComponents() { - // TODO: set up communication with the server - // this would probably be best done with the callback (so this class doesn't - // interact with the server at all) this.ingredientListView.setEditable(true); this.ingredientListView.setCellFactory( list -> { diff --git a/client/src/main/java/client/scenes/recipe/OrderedEditableListCell.java b/client/src/main/java/client/scenes/recipe/OrderedEditableListCell.java index 2a4b861..01b1e01 100644 --- a/client/src/main/java/client/scenes/recipe/OrderedEditableListCell.java +++ b/client/src/main/java/client/scenes/recipe/OrderedEditableListCell.java @@ -2,6 +2,12 @@ package client.scenes.recipe; import javafx.scene.control.ListCell; +/** + * The abstract class that pre-defines common features between + * {@link IngredientListCell IngredientListCell} and + * {@link RecipeStepListCell RecipeStepListCell}. + * @param The type of data the list cell contains. + */ public abstract class OrderedEditableListCell extends ListCell { /** * Get the display text for the given item, prefixed with its index. @@ -23,6 +29,10 @@ public abstract class OrderedEditableListCell extends ListCell { @Override public void startEdit() { + super.startEdit(); TextField textField = new TextField(this.getItem()); // Commit edit on Enter key press diff --git a/client/src/main/java/client/utils/DefaultRecipeFactory.java b/client/src/main/java/client/utils/DefaultRecipeFactory.java deleted file mode 100644 index 0a94786..0000000 --- a/client/src/main/java/client/utils/DefaultRecipeFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -package client.utils; - -import commons.Recipe; - -import java.util.List; - -public class DefaultRecipeFactory { - public static Recipe getDefaultRecipe() { - return new Recipe( - null, - "Untitled recipe", - List.of(), - List.of()); - } -} diff --git a/client/src/main/java/client/utils/DefaultValueFactory.java b/client/src/main/java/client/utils/DefaultValueFactory.java new file mode 100644 index 0000000..7d7f52d --- /dev/null +++ b/client/src/main/java/client/utils/DefaultValueFactory.java @@ -0,0 +1,63 @@ +package client.utils; + +import commons.FormalIngredient; +import commons.Ingredient; +import commons.Recipe; +import commons.Unit; +import commons.VagueIngredient; + +import java.util.List; + +/** + * A factory for default values used in the client UX flow. + * @see DefaultValueFactory#getDefaultRecipe() + * @see DefaultValueFactory#getDefaultFormalIngredient() + * @see DefaultValueFactory#getDefaultVagueIngredient() + */ +public class DefaultValueFactory { + private static final Ingredient defaultIngredient = new Ingredient(0L, "default ingredient", 0., 0., 0.); + /** + * Instantiates a recipe with a default name and null-ID. + * The ID is null such that it can be updated + * as appropriate on the backend. + * @return The default recipe. + */ + public static Recipe getDefaultRecipe() { + return new Recipe( + null, + "Untitled recipe", + List.of(), + List.of()); + } + + /** + * Instantiates a default formal ingredient with a default name and default ID. + * Note in particular the ID being 0L, see IngredientController + * server-side implementation for more details on safeguards. + * @return The default formal ingredient. + */ + public static FormalIngredient getDefaultFormalIngredient() { + return new FormalIngredient( + defaultIngredient, + 0.0, + Unit.GRAMME.suffix + ); + } + + /** + * Instantiates a vague ingredient. Not currently in use. + * @see DefaultValueFactory#getDefaultFormalIngredient() + * @return The vague ingredient. + */ + public static VagueIngredient getDefaultVagueIngredient() { + return new VagueIngredient( + defaultIngredient, + "Some"); + } + + public static VagueIngredient getDefaultVagueIngredient(String name) { + return new VagueIngredient( + new Ingredient(name, 0., 0., 0.), + "Some"); + } +} diff --git a/client/src/main/java/client/utils/ServerUtils.java b/client/src/main/java/client/utils/ServerUtils.java index 2d6dc46..2e99dbc 100644 --- a/client/src/main/java/client/utils/ServerUtils.java +++ b/client/src/main/java/client/utils/ServerUtils.java @@ -3,6 +3,7 @@ package client.utils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import commons.Recipe; +import commons.RecipeIngredient; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.ClientBuilder; import org.glassfish.jersey.client.ClientConfig; @@ -82,7 +83,10 @@ public class ServerUtils { public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException { //Make sure the name of the newRecipe is unique List allRecipes = getRecipes(); - + newRecipe.setId(null); // otherwise the id is the same as the original, and that's wrong + // now that each recipeIngredient has its own ID in the database, + // we set that to null too to force a new persist value on the server + newRecipe.getIngredients().forEach(ingredient -> ingredient.setId(null)); int version = 1; String originalName = newRecipe.getName(); @@ -143,9 +147,6 @@ public class ServerUtils { } // recipe exists so you can make a "new" recipe aka the clone Recipe recipe = objectMapper.readValue(response.body(), Recipe.class); - - recipe.setId(null); // otherwise the id is the same as the original, and that's wrong - return addRecipe(recipe); } @@ -170,8 +171,8 @@ public class ServerUtils { } - public void addRecipeIngredient(Recipe recipe, String ingredient) throws IOException, InterruptedException { - List ingredients = new ArrayList<>(recipe.getIngredients()); + public void addRecipeIngredient(Recipe recipe, RecipeIngredient ingredient) throws IOException, InterruptedException { + List ingredients = new ArrayList<>(recipe.getIngredients()); ingredients.add(ingredient); recipe.setIngredients(ingredients); diff --git a/client/src/test/java/client/ServerUtilsTest.java b/client/src/test/java/client/ServerUtilsTest.java index 47e4786..a493524 100644 --- a/client/src/test/java/client/ServerUtilsTest.java +++ b/client/src/test/java/client/ServerUtilsTest.java @@ -1,7 +1,11 @@ package client; import client.utils.ServerUtils; +import commons.Ingredient; import commons.Recipe; +import commons.RecipeIngredient; +import commons.VagueIngredient; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,6 +18,16 @@ import static org.junit.jupiter.api.Assertions.*; class ServerUtilsTest { static ServerUtils dv; static Recipe testRecipe; + static final List ingredients = List.of( + new Ingredient("Bread", 1, 2, 3), + new Ingredient("Cheese", 2, 2, 2), + new Ingredient("Ham", 3, 3, 3) + ); + static final List testIngredients = List.of( + new VagueIngredient(ingredients.get(0), "2 pieces of"), + new VagueIngredient(ingredients.get(1), "1 slice of"), + new VagueIngredient(ingredients.get(2), "1 slice of") + ); final int fakeId = -1; // If suppose ID's are only positive @BeforeEach @@ -22,25 +36,33 @@ class ServerUtilsTest { Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available"); - //Making sure there is no recipe in the backend yet - for (Recipe recipe : dv.getRecipes()) { - dv.deleteRecipe(recipe.getId()); - } - Recipe r = new Recipe(); r.setName("Tosti"); - r.setIngredients(List.of("Bread", "Cheese", "Ham")); + r.setIngredients(testIngredients); r.setPreparationSteps(List.of("Step 1:", "Step 2")); testRecipe = dv.addRecipe(r); } + @AfterEach + void tearDown() throws IOException, InterruptedException { + // Not applicable in pipeline testing + Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available"); + dv.getRecipes().stream().map(Recipe::getId).forEach(id -> { + try { + dv.deleteRecipe(id); + } catch (Exception ex) { + System.err.println("Teardown failed: " + ex.getMessage()); + } + }); + } + @Test void addRecipeTest() throws IOException, InterruptedException { Recipe r = new Recipe(); r.setName("Eggs on toast"); - r.setIngredients(List.of("Bread", "egg", "salt")); + r.setIngredients(testIngredients); r.setPreparationSteps(List.of("Step 1:", "Step 2")); testRecipe = dv.addRecipe(r); @@ -68,7 +90,7 @@ class ServerUtilsTest { assertEquals(testRecipe.getId(), r.getId()); assertEquals("Tosti", r.getName()); - assertIterableEquals(List.of("Bread", "Cheese", "Ham"), r.getIngredients()); + assertIterableEquals(testIngredients, r.getIngredients()); assertIterableEquals(List.of("Step 1:", "Step 2"), r.getPreparationSteps()); } diff --git a/client/src/test/java/client/scenes/PrintExportTest.java b/client/src/test/java/client/scenes/PrintExportTest.java index 897c88d..aee0d8d 100644 --- a/client/src/test/java/client/scenes/PrintExportTest.java +++ b/client/src/test/java/client/scenes/PrintExportTest.java @@ -1,8 +1,10 @@ package client.scenes; +import client.utils.DefaultValueFactory; import client.utils.PrintExportService; import client.utils.ServerUtils; import commons.Recipe; +import commons.RecipeIngredient; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,9 +27,9 @@ public class PrintExportTest { @Test public void buildRecipeTextTest(){ - List ingredients = new ArrayList<>(); - ingredients.add("Banana"); - ingredients.add("Bread"); + List ingredients = new ArrayList<>(); + ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Banana")); + ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Bread")); final long testRecipeId = 1234L; List preparationSteps = new ArrayList<>(); preparationSteps.add("Mix Ingredients"); @@ -37,7 +39,7 @@ public class PrintExportTest { assertEquals(""" Title: Banana Bread Recipe ID: 1234 - Ingredients: Banana, Bread,\s + Ingredients: some Banana, some Bread,\s Steps: 1: Mix Ingredients 2: Heat in Oven diff --git a/commons/src/main/java/commons/FormalIngredient.java b/commons/src/main/java/commons/FormalIngredient.java new file mode 100644 index 0000000..19f514c --- /dev/null +++ b/commons/src/main/java/commons/FormalIngredient.java @@ -0,0 +1,72 @@ +package commons; + +import jakarta.persistence.Entity; + +import java.util.Objects; +import java.util.Optional; + +/** + * A formal ingredient inheriting from base {@link RecipeIngredient RecipeIngredient}, + * holds an amount and a unit suffix in {@link String String} form. + * @see RecipeIngredient + */ +@Entity +public class FormalIngredient extends RecipeIngredient implements Scalable { + private double amount; + private String unitSuffix; + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + + public String getUnitSuffix() { + return unitSuffix; + } + + public void setUnitSuffix(String unitSuffix) { + this.unitSuffix = unitSuffix; + } + + public FormalIngredient(Ingredient ingredient, double amount, String unitSuffix) { + super(ingredient); + this.amount = amount; + this.unitSuffix = unitSuffix; + } + + public FormalIngredient() { + // ORM + } + + public double amountInBaseUnit() { + Optional unit = Unit.fromString(unitSuffix); + if (unit.isEmpty() || !unit.get().isFormal() || unit.get().conversionFactor <= 0) { + return 0.0; + } + return amount * unit.get().conversionFactor; + } + public String toString() { + return amount + unitSuffix + " of " + ingredient.name; + } + + @Override + public FormalIngredient scaleBy(double factor) { + return new FormalIngredient(getIngredient(), amount * factor, unitSuffix); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + FormalIngredient that = (FormalIngredient) o; + return Double.compare(amount, that.amount) == 0 && Objects.equals(unitSuffix, that.unitSuffix); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), amount, unitSuffix); + } +} diff --git a/commons/src/main/java/commons/Ingredient.java b/commons/src/main/java/commons/Ingredient.java index 2ecc532..207d568 100644 --- a/commons/src/main/java/commons/Ingredient.java +++ b/commons/src/main/java/commons/Ingredient.java @@ -6,6 +6,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.Objects; + @Entity public class Ingredient { @@ -20,7 +22,8 @@ public class Ingredient { @GeneratedValue(strategy = GenerationType.AUTO) public long id; - @Column(name = "name", nullable = false, unique = true) + // FIXME Dec 22 2025::temporarily made this not a unique constraint because of weird JPA behaviour + @Column(name = "name", nullable = false, unique = false) public String name; @@ -35,10 +38,24 @@ public class Ingredient { public Ingredient() {} - public Ingredient(String name, - double proteinPer100g, - double fatPer100g, - double carbsPer100g) { + public Ingredient( + Long id, + String name, + double proteinPer100g, + double fatPer100g, + double carbsPer100g) { + this.id = id; + this.name = name; + this.proteinPer100g = proteinPer100g; + this.fatPer100g = fatPer100g; + this.carbsPer100g = carbsPer100g; + } + public Ingredient( + String name, + double proteinPer100g, + double fatPer100g, + double carbsPer100g) { + this.name = name; this.proteinPer100g = proteinPer100g; this.fatPer100g = fatPer100g; @@ -54,6 +71,18 @@ public class Ingredient { public void setId(long id) { this.id = id; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Ingredient that = (Ingredient) o; + return id == that.id && Double.compare(proteinPer100g, that.proteinPer100g) == 0 && Double.compare(fatPer100g, that.fatPer100g) == 0 && Double.compare(carbsPer100g, that.carbsPer100g) == 0 && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, proteinPer100g, fatPer100g, carbsPer100g); + } } diff --git a/commons/src/main/java/commons/Recipe.java b/commons/src/main/java/commons/Recipe.java index ffe87cb..1ae1f83 100644 --- a/commons/src/main/java/commons/Recipe.java +++ b/commons/src/main/java/commons/Recipe.java @@ -15,16 +15,17 @@ */ package commons; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.Entity; import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OrderColumn; -import jakarta.persistence.GeneratedValue; import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; +import jakarta.persistence.Table; import java.util.ArrayList; @@ -59,11 +60,11 @@ public class Recipe { // | 1 (Steak) | 40g pepper | // | 1 (Steak) | Meat | // |----------------------------------| - @ElementCollection + @OneToMany @CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id")) @Column(name = "ingredient") // TODO: Replace String with Embeddable Ingredient Class - private List ingredients = new ArrayList<>(); + private List ingredients = new ArrayList<>(); // Creates another table named recipe_preparation which stores: // recipe_preparation(recipe_id -> recipes(id), preparation_step, step_order). @@ -94,7 +95,7 @@ public class Recipe { } // TODO: Replace String with Embeddable Ingredient Class for ingredients - public Recipe(Long id, String name, List ingredients, List preparationSteps) { + public Recipe(Long id, String name, List ingredients, List preparationSteps) { // Not used by JPA/Spring this.id = id; this.name = name; @@ -119,14 +120,14 @@ public class Recipe { } // TODO: Replace String with Embeddable Ingredient Class - public List getIngredients() { + public List getIngredients() { // Disallow modifying the returned list. // You can still copy it with List.copyOf(...) return Collections.unmodifiableList(ingredients); } // TODO: Replace String with Embeddable Ingredient Class - public void setIngredients(List ingredients) { + public void setIngredients(List ingredients) { this.ingredients = ingredients; } diff --git a/commons/src/main/java/commons/RecipeIngredient.java b/commons/src/main/java/commons/RecipeIngredient.java index 8b18d41..0e8653f 100644 --- a/commons/src/main/java/commons/RecipeIngredient.java +++ b/commons/src/main/java/commons/RecipeIngredient.java @@ -1,77 +1,90 @@ package commons; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -@Entity -public class RecipeIngredient { +import java.util.Objects; +/** + * The base RecipeIngredient class, holding a reference to the + * {@link Ingredient Ingredient} it has an (in)formal amount + * linked to. + * It uses {@link JsonSubTypes JsonSubTypes} such that the + * Jackson framework can conveniently decode JSON data sent + * by client, which could be either a formal or vague + * ingredient. + * @see Ingredient + */ +@Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, // identifies subtype by logical name + include = JsonTypeInfo.As.PROPERTY, + property = "type" // JSON property carrying the subtype id +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = FormalIngredient.class, name = "formal"), + @JsonSubTypes.Type(value = VagueIngredient.class, name = "vague") +}) +public abstract class RecipeIngredient { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @GeneratedValue(strategy = GenerationType.AUTO) public Long id; - // which recipe is used - @ManyToOne(optional = false) - @JoinColumn(name = "recipe_id") - public Recipe recipe; - - //which ingredient is used + /** + * Many-to-one: Many {@link RecipeIngredient RecipeIngredient} + * can use the same {@link Ingredient Ingredient} with varying + * amounts. This allows better ingredient collecting for + * nutrition view. + */ @ManyToOne(optional = false) @JoinColumn(name = "ingredient_id") public Ingredient ingredient; - public double amount; - - // store the unit name in the database - public String unitName; - @SuppressWarnings("unused") protected RecipeIngredient() { - // for sebastian + // for ORM } - public RecipeIngredient(Recipe recipe, //which recipe - Ingredient ingredient, // which ingredient - double amount, // the amount - String unit) { //gram liter etc - //store it im tha field - this.recipe = recipe; + public RecipeIngredient( + Ingredient ingredient) { + //store it in the field this.ingredient = ingredient; - this.amount = amount; - this.unitName = unit; } - // Convert unitName to Unit object so java can read it - public Unit getUnit() { - return switch (unitName) { - case "GRAM" -> Unit.GRAM; - case "KILOGRAM" -> Unit.KILOGRAM; - case "MILLILITER" -> Unit.MILLILITER; - case "LITER" -> Unit.LITER; - case "TABLESPOON" -> Unit.TABLESPOON; - case "TEASPOON" -> Unit.TEASPOON; - case "CUP" -> Unit.CUP; - case "PIECE" -> Unit.PIECE; - - case "PINCH" -> Unit.PINCH; - case "HANDFUL" -> Unit.HANDFUL; - case "TO_TASTE" -> Unit.TO_TASTE; - - default -> null; - }; + public void setId(Long id) { + this.id = id; } + public void setIngredient(Ingredient ingredient) { + this.ingredient = ingredient; + } + public Long getId() { + return id; + } - public double amountInBaseUnit() { - Unit unit = getUnit(); - if (unit == null || !unit.isFormal() || unit.conversionFactor <= 0) { - return 0.0; - } - return amount * unit.conversionFactor; + public Ingredient getIngredient() { + return ingredient; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + RecipeIngredient that = (RecipeIngredient) o; + return Objects.equals(id, that.id) && Objects.equals(ingredient, that.ingredient); + } + + @Override + public int hashCode() { + return Objects.hash(id, ingredient); } } diff --git a/commons/src/main/java/commons/Scalable.java b/commons/src/main/java/commons/Scalable.java new file mode 100644 index 0000000..f2a3981 --- /dev/null +++ b/commons/src/main/java/commons/Scalable.java @@ -0,0 +1,8 @@ +package commons; + +public interface Scalable { + default IngredientType scaleBy(int numPortions) { + return scaleBy((double) numPortions); + }; + IngredientType scaleBy(double factor); +} diff --git a/commons/src/main/java/commons/Unit.java b/commons/src/main/java/commons/Unit.java index 16627f4..7a2026a 100644 --- a/commons/src/main/java/commons/Unit.java +++ b/commons/src/main/java/commons/Unit.java @@ -1,33 +1,45 @@ package commons; -//what is a record class and why is it recommended +import java.util.Optional; + +/** + * A unit enum that holds some values. + * A {@link Unit Unit} enum holds primarily formal mass-units + * Except for an informal unit that always has a factor of zero + * is made for user input purposes. + * @see VagueIngredient + * @see FormalIngredient + */ public enum Unit { - //formal units - //weight units - GRAM("GRAM", true, 1.0), - KILOGRAM("KILOGRAM", true, 1000.0 ), + // Mass units with their absolute scale + GRAMME("g", true, 1.0), + KILOGRAMME("kg", true, 1_000.0 ), + TONNE("t", true, 1_000_000.0), - //volume units - MILLILITER("MILLILITER",true, 1.0), - LITER("LITER", true, 1000.0), - TABLESPOON("TABLESPOON",true, 15.0), - TEASPOON("TEASPOON", true, 5), - CUP ("CUP", true, 240.0), + // TODO Consider more fine-tuned volume unit definitions + // TODO Use density-based calculations? + /* + Volume units are used with the assumption that 1L of ingredient = 1kg. + */ + LITRE("l", true, 1_000.0), + MILLILITRE("ml", true, 1.0), - //piece should be a formal unit to converse for portions like 3eggs can become 1,5 eggs this way - PIECE("PIECE", true, 1.0), + // We love our American friends!! + // Source: + // https://whatscookingamerica.net/equiv.htm + TABLESPOON("tbsp", true, 14.0), + OUNCE("oz", true, 28.35), + POUND("lb", true, 453.6), + CUP("cup(s)", true, 225), + // A special informal unit + INFORMAL("", false, 0.0); - //informal units - PINCH("PINCH", false, 0.0), - HANDFUL("HANDFUL", false, 0.0), - TO_TASTE("TO_TASTE", false, 0.0); - - public final String name; + public final String suffix; public final boolean formal; public final double conversionFactor; - Unit(String name, boolean formal, double conversionFactor) { - this.name = name; + Unit(String suffix, boolean formal, double conversionFactor) { + this.suffix = suffix; this.formal = formal; this.conversionFactor = conversionFactor; } @@ -36,12 +48,16 @@ public enum Unit { return formal; } - public boolean isInformal() { - return !formal; - } //for oskar - @Override public String toString() { - return name; + return suffix; + } + public static Optional fromString(String suffix) { + for (Unit unit : Unit.values()) { + if (unit.suffix != null && unit.suffix.equals(suffix)) { + return Optional.of(unit); + } + } + return Optional.empty(); } } diff --git a/commons/src/main/java/commons/VagueIngredient.java b/commons/src/main/java/commons/VagueIngredient.java new file mode 100644 index 0000000..ca63426 --- /dev/null +++ b/commons/src/main/java/commons/VagueIngredient.java @@ -0,0 +1,48 @@ +package commons; + +import jakarta.persistence.Entity; + +import java.util.Objects; + +/** + * A vague ingredient inheriting from {@link RecipeIngredient RecipeIngredient}. + * It has a {@link String String} description that describes an informal quantity. + * The vague ingredient renders as its descriptor and its ingredient name + * conjugated by a space via {@link VagueIngredient#toString() toString()} method. + * @see RecipeIngredient + */ +@Entity +public class VagueIngredient extends RecipeIngredient { + private String description; + public VagueIngredient() {} + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public VagueIngredient(Ingredient ingredient, String description) { + super(ingredient); + this.description = description; + } + + @Override + public String toString() { + return description + " " + ingredient.name; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + VagueIngredient that = (VagueIngredient) o; + return Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hashCode(description); + } +} diff --git a/commons/src/test/java/commons/RecipeIngredientTest.java b/commons/src/test/java/commons/RecipeIngredientTest.java index 30eb676..b4b78bc 100644 --- a/commons/src/test/java/commons/RecipeIngredientTest.java +++ b/commons/src/test/java/commons/RecipeIngredientTest.java @@ -2,94 +2,56 @@ package commons; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; public class RecipeIngredientTest { - private RecipeIngredient gram; - private RecipeIngredient kilogram; - private RecipeIngredient milliliter; - private RecipeIngredient liter; - private RecipeIngredient teaspoon; - private RecipeIngredient tablespoon; - private RecipeIngredient cup; - private RecipeIngredient piece; - private RecipeIngredient pinch; - private RecipeIngredient handful; - private RecipeIngredient toTaste; - private RecipeIngredient invalid; + private Map ingredientUnitMap; + private Map ingredientDescriptorMap; + private FormalIngredient getFormal(String unit) { + Ingredient ingredient = new Ingredient("Bread", 1, 2, 3); + return new FormalIngredient(ingredient, 1.0, unit); + } + private VagueIngredient getVague(String descriptor) { + Ingredient ingredient = new Ingredient("Bread", 1, 2, 3); + return new VagueIngredient(ingredient, descriptor); + } @BeforeEach void setup(){ - Recipe recipe = new Recipe(); - Ingredient ingredient = new Ingredient(); - gram = new RecipeIngredient(recipe,ingredient,1,"GRAM"); - kilogram = new RecipeIngredient(recipe,ingredient,1,"KILOGRAM"); - - milliliter = new RecipeIngredient(recipe,ingredient,1,"MILLILITER"); - liter = new RecipeIngredient(recipe,ingredient,1,"LITER"); - teaspoon = new RecipeIngredient(recipe,ingredient,1,"TEASPOON"); - tablespoon = new RecipeIngredient(recipe,ingredient,1,"TABLESPOON"); - cup = new RecipeIngredient(recipe,ingredient,1,"CUP"); - piece = new RecipeIngredient(recipe,ingredient,1,"PIECE"); - pinch = new RecipeIngredient(recipe,ingredient,1,"PINCH"); - handful = new RecipeIngredient(recipe,ingredient,1,"HANDFUL"); - toTaste = new RecipeIngredient(recipe,ingredient,1,"TO_TASTE"); - - invalid = new RecipeIngredient(recipe,ingredient,1,"INVALID"); - } - - - @Test - void getFormalUnitTest(){ - assertEquals(Unit.GRAM, gram.getUnit()); - assertEquals(Unit.KILOGRAM, kilogram.getUnit()); - assertEquals(Unit.MILLILITER, milliliter.getUnit()); - assertEquals(Unit.LITER, liter.getUnit()); - assertEquals(Unit.TEASPOON, teaspoon.getUnit()); - assertEquals(Unit.TABLESPOON, tablespoon.getUnit()); - assertEquals(Unit.CUP, cup.getUnit()); - - assertEquals(Unit.PIECE, piece.getUnit()); + ingredientUnitMap = new HashMap<>(); + ingredientDescriptorMap = new HashMap<>(); + List.of("g", "kg", "ml", "l", "tbsp", "cup") + .forEach(u -> ingredientUnitMap.put(u, getFormal(u))); + List.of("a sprinkle of", "some", "bits of", "a few") + .forEach(d -> ingredientDescriptorMap.put(d, getVague(d))); } @Test - void getInformalUnitTest(){ - assertEquals(Unit.PINCH, pinch.getUnit()); - assertEquals(Unit.HANDFUL, handful.getUnit()); - assertEquals(Unit.TO_TASTE, toTaste.getUnit()); + void testInstantiateFormalIngredient() { + Ingredient ingredient = new Ingredient("Bread", 1, 2, 3); + FormalIngredient fi = new FormalIngredient(ingredient, 1.0, "g"); + assertEquals(ingredient, fi.getIngredient()); + } + @Test + void testInstantiateVagueIngredient() { + Ingredient ingredient = new Ingredient("Bread", 1, 2, 3); + VagueIngredient fi = new VagueIngredient(ingredient, "some"); + assertEquals("some", fi.getDescription()); } @Test - void getUnknownUnitTest(){ - assertNull(invalid.getUnit()); - } - - @Test - void convertFormalToBaseUnit(){ - assertEquals(1000, kilogram.amountInBaseUnit()); - - assertEquals(1,milliliter.amountInBaseUnit()); - assertEquals(1000.0,liter.amountInBaseUnit()); - assertEquals(15.0,tablespoon.amountInBaseUnit()); - assertEquals(5,teaspoon.amountInBaseUnit()); - assertEquals(240.0,cup.amountInBaseUnit()); - - assertEquals(1.0,piece.amountInBaseUnit()); - } - - @Test - void convertInformalToBaseUnit(){ - assertEquals(0,pinch.amountInBaseUnit()); - assertEquals(0,handful.amountInBaseUnit()); - assertEquals(0, toTaste.amountInBaseUnit()); - } - - @Test - void convertUnknownToBaseUnit(){ - assertEquals(0,invalid.amountInBaseUnit()); + void testFormalIngredientScaleByFactor() { + ingredientUnitMap.replaceAll( + (_, b) -> b.scaleBy(2) + ); + // Each amount is doubled after the mapping + assertEquals(ingredientUnitMap.size(), ingredientUnitMap.values().stream() + .filter(i -> i.getAmount() == 2.0).count()); } } diff --git a/commons/src/test/java/commons/RecipeTest.java b/commons/src/test/java/commons/RecipeTest.java index db70bf2..3834fa8 100644 --- a/commons/src/test/java/commons/RecipeTest.java +++ b/commons/src/test/java/commons/RecipeTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @@ -12,6 +13,17 @@ class RecipeTest { Recipe recipe; static final Long RECIPE_ID = 1L; + static final RecipeIngredient testIngredient = new FormalIngredient( + new Ingredient("Bread", 1, 2, 3), + 1.0, + "g" + ); + + static RecipeIngredient getTemplate(String name) { + return new VagueIngredient( + new Ingredient(name, 1.0, 1.0, 1.0), + "some"); + } @BeforeEach void setupRecipe() { @@ -42,8 +54,7 @@ class RecipeTest { @Test void getIngredientsAddThrow() { - // TODO: Change to actual Ingredient class later - assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().add("Lasagna")); + assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().add(testIngredient)); } @Test @@ -53,8 +64,10 @@ class RecipeTest { @Test void setIngredients() { - // TODO: Change to actual Ingredient class later - List ingredients = new ArrayList<>(List.of("Chocolate", "Flour", "Egg")); + List ingredients = + Stream.of("Chocolate", "Flour", "Egg") + .map(RecipeTest::getTemplate) + .toList(); recipe.setIngredients(ingredients); assertEquals(recipe.getIngredients(), ingredients); diff --git a/commons/src/test/java/commons/UnitTest.java b/commons/src/test/java/commons/UnitTest.java index 8d7f06f..d6bc9e5 100644 --- a/commons/src/test/java/commons/UnitTest.java +++ b/commons/src/test/java/commons/UnitTest.java @@ -1,6 +1,7 @@ package commons; import org.junit.jupiter.api.Test; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -11,64 +12,55 @@ class UnitTest { private static final double KILOGRAMS = 1000.0; private static final double MILLILITERS = 1.0; private static final double LITERS = 1000.0; - private static final double TABLESPOONS = 15.0; - private static final double TEASPOONS = 5.0; - private static final double CUPS = 240.0; - private static final double PIECES = 1.0; - - private static final double NoMeaningfulValue = 0.0; + private static final double TABLESPOONS = 14.0; + private static final double CUPS = 225.0; @Test void formalUnitMarkedFormal(){ - assertTrue(Unit.GRAM.isFormal()); - assertTrue(Unit.KILOGRAM.isFormal()); - assertTrue(Unit.LITER.isFormal()); + assertTrue(Unit.GRAMME.isFormal()); + assertTrue(Unit.KILOGRAMME.isFormal()); + assertTrue(Unit.LITRE.isFormal()); assertTrue(Unit.CUP.isFormal()); - assertTrue(Unit.MILLILITER.isFormal()); - assertTrue(Unit.PIECE.isFormal()); + assertTrue(Unit.MILLILITRE.isFormal()); assertTrue(Unit.TABLESPOON.isFormal()); - assertTrue(Unit.TEASPOON.isFormal()); } @Test void informalUnitAreNotFormal() { - assertFalse(Unit.PINCH.isFormal()); - assertFalse(Unit.HANDFUL.isFormal()); - assertFalse(Unit.TO_TASTE.isFormal()); + assertFalse(Unit.INFORMAL.isFormal()); } @Test void conversionIsCorrect() { - assertEquals(GRAMS, Unit.GRAM.conversionFactor); - assertEquals(KILOGRAMS, Unit.KILOGRAM.conversionFactor); - assertEquals(LITERS, Unit.LITER.conversionFactor); + assertEquals(GRAMS, Unit.GRAMME.conversionFactor); + assertEquals(KILOGRAMS, Unit.KILOGRAMME.conversionFactor); + assertEquals(LITERS, Unit.LITRE.conversionFactor); assertEquals(CUPS, Unit.CUP.conversionFactor); - assertEquals(MILLILITERS, Unit.MILLILITER.conversionFactor); - assertEquals(PIECES, Unit.PIECE.conversionFactor); + assertEquals(MILLILITERS, Unit.MILLILITRE.conversionFactor); assertEquals(TABLESPOONS, Unit.TABLESPOON.conversionFactor); - assertEquals(TEASPOONS, Unit.TEASPOON.conversionFactor); - - assertEquals(NoMeaningfulValue, Unit.PINCH.conversionFactor); - assertEquals(NoMeaningfulValue, Unit.HANDFUL.conversionFactor); - assertEquals(NoMeaningfulValue, Unit.TO_TASTE.conversionFactor); - } @Test void toStringReturnsName(){ - assertEquals("GRAM", Unit.GRAM.toString()); - assertEquals("KILOGRAM", Unit.KILOGRAM.toString()); - assertEquals("LITER", Unit.LITER.toString()); - assertEquals("CUP", Unit.CUP.toString()); - assertEquals("MILLILITER", Unit.MILLILITER.toString()); - assertEquals("PIECE", Unit.PIECE.toString()); - assertEquals("TABLESPOON", Unit.TABLESPOON.toString()); - assertEquals("TEASPOON", Unit.TEASPOON.toString()); - - assertEquals("PINCH", Unit.PINCH.toString()); - assertEquals("HANDFUL", Unit.HANDFUL.toString()); - assertEquals("TO_TASTE", Unit.TO_TASTE.toString()); + assertEquals("g", Unit.GRAMME.toString()); + assertEquals("kg", Unit.KILOGRAMME.toString()); + assertEquals("l", Unit.LITRE.toString()); + assertEquals("cup(s)", Unit.CUP.toString()); + assertEquals("ml", Unit.MILLILITRE.toString()); + assertEquals("tbsp", Unit.TABLESPOON.toString()); + } + @Test + void testFromSuffixCreatesSomeUnit() { + assertTrue(Unit.fromString("g").isPresent()); + } + @Test + void testFromSuffixCreatesCorrectUnit() { + assertEquals(Optional.of(Unit.GRAMME), Unit.fromString("g")); + } + @Test + void testFromNonExistentSuffixCreatesNoUnit() { + assertFalse(Unit.fromString("joe").isPresent()); } } diff --git a/server/src/main/java/server/api/IngredientController.java b/server/src/main/java/server/api/IngredientController.java index 531b8f3..946a62d 100644 --- a/server/src/main/java/server/api/IngredientController.java +++ b/server/src/main/java/server/api/IngredientController.java @@ -167,7 +167,7 @@ public class IngredientController { Ingredient example = new Ingredient(); example.name = ingredient.name; - if (ingredientRepository.exists(Example.of(example))) { + if (ingredientRepository.existsById(ingredient.id) || ingredientRepository.exists(Example.of(example))) { return ResponseEntity.badRequest().build(); } diff --git a/server/src/main/java/server/api/RecipeController.java b/server/src/main/java/server/api/RecipeController.java index b921bf0..2a56310 100644 --- a/server/src/main/java/server/api/RecipeController.java +++ b/server/src/main/java/server/api/RecipeController.java @@ -1,5 +1,6 @@ package server.api; +import commons.Ingredient; import commons.Recipe; import commons.ws.Topics; @@ -21,6 +22,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import server.database.IngredientRepository; +import server.database.RecipeIngredientRepository; import server.database.RecipeRepository; import java.util.List; @@ -31,11 +34,17 @@ import java.util.Optional; public class RecipeController { private final RecipeRepository recipeRepository; // JPA repository used in this controller private final SimpMessagingTemplate messagingTemplate; + private final RecipeIngredientRepository recipeIngredientRepository; + private final IngredientRepository ingredientRepository; public RecipeController(RecipeRepository recipeRepository, - SimpMessagingTemplate messagingTemplate) { + SimpMessagingTemplate messagingTemplate, + IngredientRepository ingredientRepository, + RecipeIngredientRepository recipeIngredientRepository) { this.recipeRepository = recipeRepository; this.messagingTemplate = messagingTemplate; + this.recipeIngredientRepository = recipeIngredientRepository; + this.ingredientRepository = ingredientRepository; } /** @@ -72,20 +81,33 @@ public class RecipeController { return ResponseEntity.ok(recipeRepository.findAll()); } - + private Recipe saveRecipeAndDependencies(Recipe recipe) { + recipe.getIngredients() + .forEach(recipeIngredient -> + recipeIngredient.setIngredient( + ingredientRepository + .findByName(recipeIngredient.getIngredient().name) + .orElseGet(() -> ingredientRepository.save(recipeIngredient.getIngredient()) + )) + ); + recipeIngredientRepository.saveAll(recipe.getIngredients()); + Recipe saved = recipeRepository.save(recipe); + return saved; + } /** * Mapping for POST /recipe/{id}. + * Also creates the ingredient elements if they do not exist. * @param id The recipe id to replace * @param recipe The new recipe to be replaced from the original * @return The changed recipe; returns 400 Bad Request if the recipe does not exist + * @see IngredientController#createIngredient(Ingredient) */ @PostMapping("/recipe/{id}") public ResponseEntity updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) { if (!recipeRepository.existsById(id)) { return ResponseEntity.badRequest().build(); } - - Recipe saved = recipeRepository.save(recipe); + Recipe saved = saveRecipeAndDependencies(recipe); messagingTemplate.convertAndSend(Topics.RECIPES, new UpdateRecipeMessage(saved)); return ResponseEntity.ok(saved); @@ -93,11 +115,13 @@ public class RecipeController { /** * Mapping for PUT /recipe/new. + * Includes same transient object handling as the POST handler. *

* Inserts a new recipe into the repository *

* @param recipe The new recipe as a request body * @return 200 OK with the recipe you added; or 400 Bad Request if the recipe already exists by name + * @see RecipeController#updateRecipe(Long, Recipe) */ @PutMapping("/recipe/new") public ResponseEntity createRecipe(@RequestBody Recipe recipe) { @@ -113,8 +137,7 @@ public class RecipeController { if (recipeRepository.exists(Example.of(example))) { return ResponseEntity.badRequest().build(); } - - Recipe saved = recipeRepository.save(recipe); + Recipe saved = saveRecipeAndDependencies(recipe); messagingTemplate.convertAndSend(Topics.RECIPES, new CreateRecipeMessage(saved)); return ResponseEntity.ok(saved); diff --git a/server/src/main/java/server/database/IngredientRepository.java b/server/src/main/java/server/database/IngredientRepository.java index ea32d67..df54bd3 100644 --- a/server/src/main/java/server/database/IngredientRepository.java +++ b/server/src/main/java/server/database/IngredientRepository.java @@ -6,9 +6,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface IngredientRepository extends JpaRepository { List findAllByOrderByNameAsc(); Page findAllByOrderByNameAsc(Pageable pageable); + Optional findByName(String name); } diff --git a/server/src/test/java/server/api/RecipeControllerTest.java b/server/src/test/java/server/api/RecipeControllerTest.java index 9b27d2c..c16490c 100644 --- a/server/src/test/java/server/api/RecipeControllerTest.java +++ b/server/src/test/java/server/api/RecipeControllerTest.java @@ -13,6 +13,8 @@ import org.springframework.http.HttpStatus; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.test.context.ActiveProfiles; import server.WebSocketConfig; +import server.database.IngredientRepository; +import server.database.RecipeIngredientRepository; import server.database.RecipeRepository; import java.util.ArrayList; @@ -45,12 +47,21 @@ public class RecipeControllerTest { private final RecipeRepository recipeRepository; private List recipeIds; public static final int NUM_RECIPES = 10; + private final IngredientRepository ingredientRepository; + private final RecipeIngredientRepository recipeIngredientRepository; // Injects a test repository into the test class @Autowired - public RecipeControllerTest(RecipeRepository recipeRepository, SimpMessagingTemplate template) { + public RecipeControllerTest( + RecipeRepository recipeRepository, + SimpMessagingTemplate template, + IngredientRepository ingredientRepository, + RecipeIngredientRepository recipeIngredientRepository + ) { this.recipeRepository = recipeRepository; this.template = template; + this.ingredientRepository = ingredientRepository; + this.recipeIngredientRepository = recipeIngredientRepository; } @BeforeEach @@ -59,7 +70,12 @@ public class RecipeControllerTest { .range(0, NUM_RECIPES) .mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of())) .toList(); - controller = new RecipeController(recipeRepository, template); + controller = new RecipeController( + recipeRepository, + template, + ingredientRepository, + recipeIngredientRepository + ); Set tags = info.getTags(); List ids = new ArrayList<>();