Merge branch 'commons/recipe-model-v2' into 'main'
refactor: New recipe & ingredients model to work with (in)formal quantities Closes #43 See merge request cse1105/2025-2026/teams/csep-team-76!38
This commit is contained in:
commit
9f67800372
25 changed files with 606 additions and 281 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Recipe> recipes;
|
||||
private HashMap<String, Integer> ingredientStats;
|
||||
public ListView<String> nutritionIngredientsView;
|
||||
private HashMap<Ingredient, Integer> ingredientStats;
|
||||
public ListView<Ingredient> 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<Recipe> recipeList
|
||||
) {
|
||||
recipeList.forEach(recipe -> {
|
||||
Set<String> uniqueIngredients = new HashSet<>(recipe.getIngredients());
|
||||
Set<Ingredient> uniqueIngredients = new HashSet<>(
|
||||
recipe.getIngredients().stream().map(
|
||||
ingredient -> ingredient.ingredient).toList());
|
||||
nutritionIngredientsView.getItems().setAll(uniqueIngredients);
|
||||
uniqueIngredients.forEach(ingredient -> {
|
||||
ingredientStats.put(ingredient, 0);
|
||||
|
|
|
|||
|
|
@ -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<String>} for displaying and editing ingredients in an
|
||||
* IngredientList. Allows inline editing of ingredient names.
|
||||
* A custom {@link OrderedEditableListCell<String>} for
|
||||
* displaying and editing ingredients in an IngredientList.
|
||||
* Allows inline editing of ingredient names.
|
||||
*
|
||||
* @see IngredientListCtrl
|
||||
*/
|
||||
public class IngredientListCell extends OrderedEditableListCell<String> {
|
||||
public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient> {
|
||||
@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<Double> templateAmount = Optional.empty();
|
||||
Optional<Unit> 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<Unit> 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<Unit> 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<String> {
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* </p>
|
||||
*/
|
||||
private ObservableList<String> ingredients;
|
||||
private ObservableList<RecipeIngredient> ingredients;
|
||||
|
||||
/**
|
||||
* A callback function that is called when the ingredient list is updated.
|
||||
*/
|
||||
private Consumer<List<String>> updateCallback;
|
||||
private Consumer<List<RecipeIngredient>> updateCallback;
|
||||
|
||||
@FXML
|
||||
public ListView<String> ingredientListView;
|
||||
public ListView<RecipeIngredient> 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<String> ingredientList = recipe.getIngredients();
|
||||
List<RecipeIngredient> 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<List<String>> callback) {
|
||||
public void setUpdateCallback(Consumer<List<RecipeIngredient>> 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<String> event) {
|
||||
private void handleIngredientEdit(EditEvent<RecipeIngredient> 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 -> {
|
||||
|
|
|
|||
|
|
@ -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 <DataType> The type of data the list cell contains.
|
||||
*/
|
||||
public abstract class OrderedEditableListCell<DataType> extends ListCell<DataType> {
|
||||
/**
|
||||
* Get the display text for the given item, prefixed with its index.
|
||||
|
|
@ -23,6 +29,10 @@ public abstract class OrderedEditableListCell<DataType> extends ListCell<DataTyp
|
|||
this.setText(this.getDisplayText(item.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A method stub that should be supplemented by child classes.
|
||||
*/
|
||||
public void startEdit() {
|
||||
super.startEdit();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import javafx.scene.input.KeyCode;
|
|||
/**
|
||||
* A custom ListCell for displaying and editing ingredients in an
|
||||
* RecipeStepList. Allows inline editing of ingredient names.
|
||||
*
|
||||
* The RecipeStepList only needs one {@link TextField TextField} element.
|
||||
* @see IngredientListCtrl
|
||||
* @see RecipeStepListCtrl
|
||||
*/
|
||||
public class RecipeStepListCell extends OrderedEditableListCell<String> {
|
||||
@Override
|
||||
public void startEdit() {
|
||||
super.startEdit();
|
||||
TextField textField = new TextField(this.getItem());
|
||||
|
||||
// Commit edit on Enter key press
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
63
client/src/main/java/client/utils/DefaultValueFactory.java
Normal file
63
client/src/main/java/client/utils/DefaultValueFactory.java
Normal file
|
|
@ -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 <code>null</code> 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 <code>0L</code>, 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Recipe> 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<String> ingredients = new ArrayList<>(recipe.getIngredients());
|
||||
public void addRecipeIngredient(Recipe recipe, RecipeIngredient ingredient) throws IOException, InterruptedException {
|
||||
List<RecipeIngredient> ingredients = new ArrayList<>(recipe.getIngredients());
|
||||
ingredients.add(ingredient);
|
||||
recipe.setIngredients(ingredients);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Ingredient> ingredients = List.of(
|
||||
new Ingredient("Bread", 1, 2, 3),
|
||||
new Ingredient("Cheese", 2, 2, 2),
|
||||
new Ingredient("Ham", 3, 3, 3)
|
||||
);
|
||||
static final List<RecipeIngredient> 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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String> ingredients = new ArrayList<>();
|
||||
ingredients.add("Banana");
|
||||
ingredients.add("Bread");
|
||||
List<RecipeIngredient> ingredients = new ArrayList<>();
|
||||
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Banana"));
|
||||
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Bread"));
|
||||
final long testRecipeId = 1234L;
|
||||
List<String> 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
|
||||
|
|
|
|||
72
commons/src/main/java/commons/FormalIngredient.java
Normal file
72
commons/src/main/java/commons/FormalIngredient.java
Normal file
|
|
@ -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<FormalIngredient> {
|
||||
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 = 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String> ingredients = new ArrayList<>();
|
||||
private List<RecipeIngredient> 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<String> ingredients, List<String> preparationSteps) {
|
||||
public Recipe(Long id, String name, List<RecipeIngredient> ingredients, List<String> 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<String> getIngredients() {
|
||||
public List<RecipeIngredient> 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<String> ingredients) {
|
||||
public void setIngredients(List<RecipeIngredient> ingredients) {
|
||||
this.ingredients = ingredients;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
commons/src/main/java/commons/Scalable.java
Normal file
8
commons/src/main/java/commons/Scalable.java
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package commons;
|
||||
|
||||
public interface Scalable<IngredientType extends RecipeIngredient> {
|
||||
default IngredientType scaleBy(int numPortions) {
|
||||
return scaleBy((double) numPortions);
|
||||
};
|
||||
IngredientType scaleBy(double factor);
|
||||
}
|
||||
|
|
@ -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("<NONE>", 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<Unit> fromString(String suffix) {
|
||||
for (Unit unit : Unit.values()) {
|
||||
if (unit.suffix != null && unit.suffix.equals(suffix)) {
|
||||
return Optional.of(unit);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
commons/src/main/java/commons/VagueIngredient.java
Normal file
48
commons/src/main/java/commons/VagueIngredient.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, FormalIngredient> ingredientUnitMap;
|
||||
private Map<String, VagueIngredient> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> ingredients = new ArrayList<>(List.of("Chocolate", "Flour", "Egg"));
|
||||
List<RecipeIngredient> ingredients =
|
||||
Stream.of("Chocolate", "Flour", "Egg")
|
||||
.map(RecipeTest::getTemplate)
|
||||
.toList();
|
||||
recipe.setIngredients(ingredients);
|
||||
|
||||
assertEquals(recipe.getIngredients(), ingredients);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <code>POST /recipe/{id}</code>.
|
||||
* 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<Recipe> 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 <code>PUT /recipe/new</code>.
|
||||
* Includes same transient object handling as the POST handler.
|
||||
* <p>
|
||||
* Inserts a new recipe into the repository
|
||||
* </p>
|
||||
* @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<Recipe> 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);
|
||||
|
|
|
|||
|
|
@ -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<Ingredient, Long> {
|
||||
List<Ingredient> findAllByOrderByNameAsc();
|
||||
Page<Ingredient> findAllByOrderByNameAsc(Pageable pageable);
|
||||
Optional<Ingredient> findByName(String name);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Long> 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<String> tags = info.getTags();
|
||||
List<Long> ids = new ArrayList<>();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue