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<>();