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.Config;
|
||||||
import client.utils.ConfigService;
|
import client.utils.ConfigService;
|
||||||
import client.utils.DefaultRecipeFactory;
|
import client.utils.DefaultValueFactory;
|
||||||
import client.utils.LocaleAware;
|
import client.utils.LocaleAware;
|
||||||
import client.utils.LocaleManager;
|
import client.utils.LocaleManager;
|
||||||
import client.utils.ServerUtils;
|
import client.utils.ServerUtils;
|
||||||
|
|
@ -217,7 +217,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void addRecipe() {
|
private void addRecipe() {
|
||||||
Recipe newRecipe = DefaultRecipeFactory.getDefaultRecipe(); // Create default recipe
|
Recipe newRecipe = DefaultValueFactory.getDefaultRecipe(); // Create default recipe
|
||||||
try {
|
try {
|
||||||
newRecipe = server.addRecipe(newRecipe); // get the new recipe id
|
newRecipe = server.addRecipe(newRecipe); // get the new recipe id
|
||||||
refresh(); // refresh view with server recipes
|
refresh(); // refresh view with server recipes
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package client.scenes.nutrition;
|
||||||
|
|
||||||
import client.scenes.FoodpalApplicationCtrl;
|
import client.scenes.FoodpalApplicationCtrl;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import commons.Ingredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
@ -14,12 +15,12 @@ import java.util.Set;
|
||||||
|
|
||||||
public class NutritionViewCtrl {
|
public class NutritionViewCtrl {
|
||||||
private ObservableList<Recipe> recipes;
|
private ObservableList<Recipe> recipes;
|
||||||
private HashMap<String, Integer> ingredientStats;
|
private HashMap<Ingredient, Integer> ingredientStats;
|
||||||
public ListView<String> nutritionIngredientsView;
|
public ListView<Ingredient> nutritionIngredientsView;
|
||||||
private final NutritionDetailsCtrl nutritionDetailsCtrl;
|
private final NutritionDetailsCtrl nutritionDetailsCtrl;
|
||||||
|
|
||||||
// TODO into Ingredient class definition
|
// 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.
|
* Comedically verbose function to count unique appearances of an ingredient by name in each recipe.
|
||||||
* For each recipe:
|
* For each recipe:
|
||||||
|
|
@ -29,13 +30,15 @@ public class NutritionViewCtrl {
|
||||||
* 2. For each recipe in list:
|
* 2. For each recipe in list:
|
||||||
* 1. If the name of the ingredient exists in the recipe list, increment the statistic by 1.
|
* 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.
|
* 2. Else maintain the same value for that statistic.
|
||||||
* @param recipeList
|
* @param recipeList The recipe list
|
||||||
*/
|
*/
|
||||||
private void updateIngredientStats(
|
private void updateIngredientStats(
|
||||||
List<Recipe> recipeList
|
List<Recipe> recipeList
|
||||||
) {
|
) {
|
||||||
recipeList.forEach(recipe -> {
|
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);
|
nutritionIngredientsView.getItems().setAll(uniqueIngredients);
|
||||||
uniqueIngredients.forEach(ingredient -> {
|
uniqueIngredients.forEach(ingredient -> {
|
||||||
ingredientStats.put(ingredient, 0);
|
ingredientStats.put(ingredient, 0);
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,71 @@
|
||||||
package client.scenes.recipe;
|
package client.scenes.recipe;
|
||||||
|
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import commons.Ingredient;
|
||||||
|
import commons.RecipeIngredient;
|
||||||
import commons.Unit;
|
import commons.Unit;
|
||||||
|
import commons.VagueIngredient;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.scene.control.ChoiceBox;
|
import javafx.scene.control.ChoiceBox;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom {@link OrderedEditableListCell<String>} for displaying and editing ingredients in an
|
* A custom {@link OrderedEditableListCell<String>} for
|
||||||
* IngredientList. Allows inline editing of ingredient names.
|
* displaying and editing ingredients in an IngredientList.
|
||||||
|
* Allows inline editing of ingredient names.
|
||||||
*
|
*
|
||||||
* @see IngredientListCtrl
|
* @see IngredientListCtrl
|
||||||
*/
|
*/
|
||||||
public class IngredientListCell extends OrderedEditableListCell<String> {
|
public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient> {
|
||||||
@Override
|
@Override
|
||||||
public void commitEdit(String newValue) {
|
public void commitEdit(RecipeIngredient newValue) {
|
||||||
super.commitEdit(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
|
@Override
|
||||||
public void startEdit() {
|
public void startEdit() {
|
||||||
super.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<>();
|
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);
|
HBox container = new HBox(amountInput, unitChoice, nameInput);
|
||||||
container.setOnKeyReleased(event -> {
|
container.setOnKeyReleased(event -> {
|
||||||
if (event.getCode() != KeyCode.ENTER) {
|
if (event.getCode() != KeyCode.ENTER) {
|
||||||
|
|
@ -34,15 +73,16 @@ public class IngredientListCell extends OrderedEditableListCell<String> {
|
||||||
}
|
}
|
||||||
Unit unit = unitChoice.getValue();
|
Unit unit = unitChoice.getValue();
|
||||||
String name = nameInput.getText();
|
String name = nameInput.getText();
|
||||||
if (unit == null) {
|
if (unit == null || !unit.isFormal()) {
|
||||||
String desc = amountInput.getText();
|
String desc = amountInput.getText();
|
||||||
commitEdit(desc + " " + name);
|
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.);
|
||||||
|
commitEdit(new VagueIngredient(newIngredient, desc));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int amount = Integer.parseInt(amountInput.getText());
|
double amount = Double.parseDouble(amountInput.getText());
|
||||||
commitEdit(amount + " " + unit + " of " + name);
|
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.);
|
||||||
|
commitEdit(new FormalIngredient(newIngredient, amount, unit.suffix));
|
||||||
});
|
});
|
||||||
this.setText(null);
|
return container;
|
||||||
this.setGraphic(container);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
package client.scenes.recipe;
|
package client.scenes.recipe;
|
||||||
|
|
||||||
|
import client.utils.DefaultValueFactory;
|
||||||
import client.utils.LocaleAware;
|
import client.utils.LocaleAware;
|
||||||
import client.utils.LocaleManager;
|
import client.utils.LocaleManager;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import commons.RecipeIngredient;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
|
|
@ -40,15 +44,15 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
* changes and update the recipe accordingly.
|
* changes and update the recipe accordingly.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
private ObservableList<String> ingredients;
|
private ObservableList<RecipeIngredient> ingredients;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback function that is called when the ingredient list is updated.
|
* A callback function that is called when the ingredient list is updated.
|
||||||
*/
|
*/
|
||||||
private Consumer<List<String>> updateCallback;
|
private Consumer<List<RecipeIngredient>> updateCallback;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public ListView<String> ingredientListView;
|
public ListView<RecipeIngredient> ingredientListView;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public Label ingredientsLabel;
|
public Label ingredientsLabel;
|
||||||
|
|
@ -69,7 +73,7 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
if (recipe == null) {
|
if (recipe == null) {
|
||||||
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
|
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
|
||||||
} else {
|
} else {
|
||||||
List<String> ingredientList = recipe.getIngredients();
|
List<RecipeIngredient> ingredientList = recipe.getIngredients();
|
||||||
this.ingredients = FXCollections.observableArrayList(ingredientList);
|
this.ingredients = FXCollections.observableArrayList(ingredientList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,7 +86,7 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
*
|
*
|
||||||
* @param callback The function to call upon each update.
|
* @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;
|
this.updateCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,12 +103,13 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
* @param event The action event.
|
* @param event The action event.
|
||||||
*/
|
*/
|
||||||
private void handleIngredientAdd(ActionEvent 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.refresh();
|
||||||
this.updateCallback.accept(this.ingredients);
|
this.updateCallback.accept(this.ingredients);
|
||||||
|
|
||||||
var select = this.ingredientListView.getSelectionModel();
|
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.
|
* @param event The edit event.
|
||||||
*/
|
*/
|
||||||
private void handleIngredientEdit(EditEvent<String> event) {
|
private void handleIngredientEdit(EditEvent<RecipeIngredient> event) {
|
||||||
int index = event.getIndex();
|
int index = event.getIndex();
|
||||||
String newValue = event.getNewValue();
|
RecipeIngredient newValue = event.getNewValue();
|
||||||
|
|
||||||
this.ingredients.set(index, newValue);
|
this.ingredients.set(index, newValue);
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
|
@ -154,9 +159,6 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initializeComponents() {
|
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.setEditable(true);
|
||||||
this.ingredientListView.setCellFactory(
|
this.ingredientListView.setCellFactory(
|
||||||
list -> {
|
list -> {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@ package client.scenes.recipe;
|
||||||
|
|
||||||
import javafx.scene.control.ListCell;
|
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> {
|
public abstract class OrderedEditableListCell<DataType> extends ListCell<DataType> {
|
||||||
/**
|
/**
|
||||||
* Get the display text for the given item, prefixed with its index.
|
* 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()));
|
this.setText(this.getDisplayText(item.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method stub that should be supplemented by child classes.
|
||||||
|
*/
|
||||||
public void startEdit() {
|
public void startEdit() {
|
||||||
super.startEdit();
|
super.startEdit();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ import javafx.scene.input.KeyCode;
|
||||||
/**
|
/**
|
||||||
* A custom ListCell for displaying and editing ingredients in an
|
* A custom ListCell for displaying and editing ingredients in an
|
||||||
* RecipeStepList. Allows inline editing of ingredient names.
|
* RecipeStepList. Allows inline editing of ingredient names.
|
||||||
*
|
* The RecipeStepList only needs one {@link TextField TextField} element.
|
||||||
|
* @see IngredientListCtrl
|
||||||
* @see RecipeStepListCtrl
|
* @see RecipeStepListCtrl
|
||||||
*/
|
*/
|
||||||
public class RecipeStepListCell extends OrderedEditableListCell<String> {
|
public class RecipeStepListCell extends OrderedEditableListCell<String> {
|
||||||
@Override
|
@Override
|
||||||
public void startEdit() {
|
public void startEdit() {
|
||||||
|
super.startEdit();
|
||||||
TextField textField = new TextField(this.getItem());
|
TextField textField = new TextField(this.getItem());
|
||||||
|
|
||||||
// Commit edit on Enter key press
|
// 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.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
|
import commons.RecipeIngredient;
|
||||||
import jakarta.ws.rs.ProcessingException;
|
import jakarta.ws.rs.ProcessingException;
|
||||||
import jakarta.ws.rs.client.ClientBuilder;
|
import jakarta.ws.rs.client.ClientBuilder;
|
||||||
import org.glassfish.jersey.client.ClientConfig;
|
import org.glassfish.jersey.client.ClientConfig;
|
||||||
|
|
@ -82,7 +83,10 @@ public class ServerUtils {
|
||||||
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
|
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
|
||||||
//Make sure the name of the newRecipe is unique
|
//Make sure the name of the newRecipe is unique
|
||||||
List<Recipe> allRecipes = getRecipes();
|
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;
|
int version = 1;
|
||||||
String originalName = newRecipe.getName();
|
String originalName = newRecipe.getName();
|
||||||
|
|
@ -143,9 +147,6 @@ public class ServerUtils {
|
||||||
}
|
}
|
||||||
// recipe exists so you can make a "new" recipe aka the clone
|
// recipe exists so you can make a "new" recipe aka the clone
|
||||||
Recipe recipe = objectMapper.readValue(response.body(), Recipe.class);
|
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);
|
return addRecipe(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,8 +171,8 @@ public class ServerUtils {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addRecipeIngredient(Recipe recipe, String ingredient) throws IOException, InterruptedException {
|
public void addRecipeIngredient(Recipe recipe, RecipeIngredient ingredient) throws IOException, InterruptedException {
|
||||||
List<String> ingredients = new ArrayList<>(recipe.getIngredients());
|
List<RecipeIngredient> ingredients = new ArrayList<>(recipe.getIngredients());
|
||||||
ingredients.add(ingredient);
|
ingredients.add(ingredient);
|
||||||
recipe.setIngredients(ingredients);
|
recipe.setIngredients(ingredients);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package client;
|
package client;
|
||||||
|
|
||||||
import client.utils.ServerUtils;
|
import client.utils.ServerUtils;
|
||||||
|
import commons.Ingredient;
|
||||||
import commons.Recipe;
|
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.Assumptions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -14,6 +18,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||||
class ServerUtilsTest {
|
class ServerUtilsTest {
|
||||||
static ServerUtils dv;
|
static ServerUtils dv;
|
||||||
static Recipe testRecipe;
|
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
|
final int fakeId = -1; // If suppose ID's are only positive
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
|
@ -22,25 +36,33 @@ class ServerUtilsTest {
|
||||||
|
|
||||||
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
|
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();
|
Recipe r = new Recipe();
|
||||||
|
|
||||||
r.setName("Tosti");
|
r.setName("Tosti");
|
||||||
r.setIngredients(List.of("Bread", "Cheese", "Ham"));
|
r.setIngredients(testIngredients);
|
||||||
r.setPreparationSteps(List.of("Step 1:", "Step 2"));
|
r.setPreparationSteps(List.of("Step 1:", "Step 2"));
|
||||||
|
|
||||||
testRecipe = dv.addRecipe(r);
|
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
|
@Test
|
||||||
void addRecipeTest() throws IOException, InterruptedException {
|
void addRecipeTest() throws IOException, InterruptedException {
|
||||||
Recipe r = new Recipe();
|
Recipe r = new Recipe();
|
||||||
r.setName("Eggs on toast");
|
r.setName("Eggs on toast");
|
||||||
r.setIngredients(List.of("Bread", "egg", "salt"));
|
r.setIngredients(testIngredients);
|
||||||
r.setPreparationSteps(List.of("Step 1:", "Step 2"));
|
r.setPreparationSteps(List.of("Step 1:", "Step 2"));
|
||||||
|
|
||||||
testRecipe = dv.addRecipe(r);
|
testRecipe = dv.addRecipe(r);
|
||||||
|
|
@ -68,7 +90,7 @@ class ServerUtilsTest {
|
||||||
|
|
||||||
assertEquals(testRecipe.getId(), r.getId());
|
assertEquals(testRecipe.getId(), r.getId());
|
||||||
assertEquals("Tosti", r.getName());
|
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());
|
assertIterableEquals(List.of("Step 1:", "Step 2"), r.getPreparationSteps());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package client.scenes;
|
package client.scenes;
|
||||||
|
|
||||||
|
import client.utils.DefaultValueFactory;
|
||||||
import client.utils.PrintExportService;
|
import client.utils.PrintExportService;
|
||||||
import client.utils.ServerUtils;
|
import client.utils.ServerUtils;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
|
import commons.RecipeIngredient;
|
||||||
import org.junit.jupiter.api.Assumptions;
|
import org.junit.jupiter.api.Assumptions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -25,9 +27,9 @@ public class PrintExportTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void buildRecipeTextTest(){
|
public void buildRecipeTextTest(){
|
||||||
List<String> ingredients = new ArrayList<>();
|
List<RecipeIngredient> ingredients = new ArrayList<>();
|
||||||
ingredients.add("Banana");
|
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Banana"));
|
||||||
ingredients.add("Bread");
|
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Bread"));
|
||||||
final long testRecipeId = 1234L;
|
final long testRecipeId = 1234L;
|
||||||
List<String> preparationSteps = new ArrayList<>();
|
List<String> preparationSteps = new ArrayList<>();
|
||||||
preparationSteps.add("Mix Ingredients");
|
preparationSteps.add("Mix Ingredients");
|
||||||
|
|
@ -37,7 +39,7 @@ public class PrintExportTest {
|
||||||
assertEquals("""
|
assertEquals("""
|
||||||
Title: Banana Bread
|
Title: Banana Bread
|
||||||
Recipe ID: 1234
|
Recipe ID: 1234
|
||||||
Ingredients: Banana, Bread,\s
|
Ingredients: some Banana, some Bread,\s
|
||||||
Steps:
|
Steps:
|
||||||
1: Mix Ingredients
|
1: Mix Ingredients
|
||||||
2: Heat in Oven
|
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.GenerationType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class Ingredient {
|
public class Ingredient {
|
||||||
|
|
@ -20,7 +22,8 @@ public class Ingredient {
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
public long id;
|
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;
|
public String name;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -35,10 +38,24 @@ public class Ingredient {
|
||||||
|
|
||||||
public Ingredient() {}
|
public Ingredient() {}
|
||||||
|
|
||||||
public Ingredient(String name,
|
public Ingredient(
|
||||||
|
Long id,
|
||||||
|
String name,
|
||||||
double proteinPer100g,
|
double proteinPer100g,
|
||||||
double fatPer100g,
|
double fatPer100g,
|
||||||
double carbsPer100g) {
|
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.name = name;
|
||||||
this.proteinPer100g = proteinPer100g;
|
this.proteinPer100g = proteinPer100g;
|
||||||
this.fatPer100g = fatPer100g;
|
this.fatPer100g = fatPer100g;
|
||||||
|
|
@ -54,6 +71,18 @@ public class Ingredient {
|
||||||
public void setId(long id) {
|
public void setId(long id) {
|
||||||
this.id = 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;
|
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.CollectionTable;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.OrderColumn;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.ElementCollection;
|
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;
|
import java.util.ArrayList;
|
||||||
|
|
@ -59,11 +60,11 @@ public class Recipe {
|
||||||
// | 1 (Steak) | 40g pepper |
|
// | 1 (Steak) | 40g pepper |
|
||||||
// | 1 (Steak) | Meat |
|
// | 1 (Steak) | Meat |
|
||||||
// |----------------------------------|
|
// |----------------------------------|
|
||||||
@ElementCollection
|
@OneToMany
|
||||||
@CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id"))
|
@CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id"))
|
||||||
@Column(name = "ingredient")
|
@Column(name = "ingredient")
|
||||||
// TODO: Replace String with Embeddable Ingredient Class
|
// 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:
|
// Creates another table named recipe_preparation which stores:
|
||||||
// recipe_preparation(recipe_id -> recipes(id), preparation_step, step_order).
|
// 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
|
// 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
|
// Not used by JPA/Spring
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|
@ -119,14 +120,14 @@ public class Recipe {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace String with Embeddable Ingredient Class
|
// TODO: Replace String with Embeddable Ingredient Class
|
||||||
public List<String> getIngredients() {
|
public List<RecipeIngredient> getIngredients() {
|
||||||
// Disallow modifying the returned list.
|
// Disallow modifying the returned list.
|
||||||
// You can still copy it with List.copyOf(...)
|
// You can still copy it with List.copyOf(...)
|
||||||
return Collections.unmodifiableList(ingredients);
|
return Collections.unmodifiableList(ingredients);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace String with Embeddable Ingredient Class
|
// TODO: Replace String with Embeddable Ingredient Class
|
||||||
public void setIngredients(List<String> ingredients) {
|
public void setIngredients(List<RecipeIngredient> ingredients) {
|
||||||
this.ingredients = ingredients;
|
this.ingredients = ingredients;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,90 @@
|
||||||
package commons;
|
package commons;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.GenerationType;
|
import jakarta.persistence.GenerationType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Inheritance;
|
||||||
|
import jakarta.persistence.InheritanceType;
|
||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
|
|
||||||
@Entity
|
import java.util.Objects;
|
||||||
public class RecipeIngredient {
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
public Long id;
|
public Long id;
|
||||||
|
|
||||||
// which recipe is used
|
/**
|
||||||
@ManyToOne(optional = false)
|
* Many-to-one: Many {@link RecipeIngredient RecipeIngredient}
|
||||||
@JoinColumn(name = "recipe_id")
|
* can use the same {@link Ingredient Ingredient} with varying
|
||||||
public Recipe recipe;
|
* amounts. This allows better ingredient collecting for
|
||||||
|
* nutrition view.
|
||||||
//which ingredient is used
|
*/
|
||||||
@ManyToOne(optional = false)
|
@ManyToOne(optional = false)
|
||||||
@JoinColumn(name = "ingredient_id")
|
@JoinColumn(name = "ingredient_id")
|
||||||
public Ingredient ingredient;
|
public Ingredient ingredient;
|
||||||
|
|
||||||
public double amount;
|
|
||||||
|
|
||||||
// store the unit name in the database
|
|
||||||
public String unitName;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
protected RecipeIngredient() {
|
protected RecipeIngredient() {
|
||||||
// for sebastian
|
// for ORM
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipeIngredient(Recipe recipe, //which recipe
|
public RecipeIngredient(
|
||||||
Ingredient ingredient, // which ingredient
|
Ingredient ingredient) {
|
||||||
double amount, // the amount
|
//store it in the field
|
||||||
String unit) { //gram liter etc
|
|
||||||
//store it im tha field
|
|
||||||
this.recipe = recipe;
|
|
||||||
this.ingredient = ingredient;
|
this.ingredient = ingredient;
|
||||||
this.amount = amount;
|
|
||||||
this.unitName = unit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert unitName to Unit object so java can read it
|
public void setId(Long id) {
|
||||||
public Unit getUnit() {
|
this.id = id;
|
||||||
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 setIngredient(Ingredient ingredient) {
|
||||||
|
this.ingredient = ingredient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
public double amountInBaseUnit() {
|
public Ingredient getIngredient() {
|
||||||
Unit unit = getUnit();
|
return ingredient;
|
||||||
if (unit == null || !unit.isFormal() || unit.conversionFactor <= 0) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
return amount * unit.conversionFactor;
|
|
||||||
|
@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;
|
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 {
|
public enum Unit {
|
||||||
//formal units
|
// Mass units with their absolute scale
|
||||||
//weight units
|
GRAMME("g", true, 1.0),
|
||||||
GRAM("GRAM", true, 1.0),
|
KILOGRAMME("kg", true, 1_000.0 ),
|
||||||
KILOGRAM("KILOGRAM", true, 1000.0 ),
|
TONNE("t", true, 1_000_000.0),
|
||||||
|
|
||||||
//volume units
|
// TODO Consider more fine-tuned volume unit definitions
|
||||||
MILLILITER("MILLILITER",true, 1.0),
|
// TODO Use density-based calculations?
|
||||||
LITER("LITER", true, 1000.0),
|
/*
|
||||||
TABLESPOON("TABLESPOON",true, 15.0),
|
Volume units are used with the assumption that 1L of ingredient = 1kg.
|
||||||
TEASPOON("TEASPOON", true, 5),
|
*/
|
||||||
CUP ("CUP", true, 240.0),
|
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
|
// We love our American friends!!
|
||||||
PIECE("PIECE", true, 1.0),
|
// 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
|
public final String suffix;
|
||||||
PINCH("PINCH", false, 0.0),
|
|
||||||
HANDFUL("HANDFUL", false, 0.0),
|
|
||||||
TO_TASTE("TO_TASTE", false, 0.0);
|
|
||||||
|
|
||||||
public final String name;
|
|
||||||
public final boolean formal;
|
public final boolean formal;
|
||||||
public final double conversionFactor;
|
public final double conversionFactor;
|
||||||
|
|
||||||
Unit(String name, boolean formal, double conversionFactor) {
|
Unit(String suffix, boolean formal, double conversionFactor) {
|
||||||
this.name = name;
|
this.suffix = suffix;
|
||||||
this.formal = formal;
|
this.formal = formal;
|
||||||
this.conversionFactor = conversionFactor;
|
this.conversionFactor = conversionFactor;
|
||||||
}
|
}
|
||||||
|
|
@ -36,12 +48,16 @@ public enum Unit {
|
||||||
return formal;
|
return formal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isInformal() {
|
|
||||||
return !formal;
|
|
||||||
} //for oskar
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
|
|
||||||
public class RecipeIngredientTest {
|
public class RecipeIngredientTest {
|
||||||
|
|
||||||
private RecipeIngredient gram;
|
private Map<String, FormalIngredient> ingredientUnitMap;
|
||||||
private RecipeIngredient kilogram;
|
private Map<String, VagueIngredient> ingredientDescriptorMap;
|
||||||
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 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
|
@BeforeEach
|
||||||
void setup(){
|
void setup(){
|
||||||
Recipe recipe = new Recipe();
|
ingredientUnitMap = new HashMap<>();
|
||||||
Ingredient ingredient = new Ingredient();
|
ingredientDescriptorMap = new HashMap<>();
|
||||||
gram = new RecipeIngredient(recipe,ingredient,1,"GRAM");
|
List.of("g", "kg", "ml", "l", "tbsp", "cup")
|
||||||
kilogram = new RecipeIngredient(recipe,ingredient,1,"KILOGRAM");
|
.forEach(u -> ingredientUnitMap.put(u, getFormal(u)));
|
||||||
|
List.of("a sprinkle of", "some", "bits of", "a few")
|
||||||
milliliter = new RecipeIngredient(recipe,ingredient,1,"MILLILITER");
|
.forEach(d -> ingredientDescriptorMap.put(d, getVague(d)));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getInformalUnitTest(){
|
void testInstantiateFormalIngredient() {
|
||||||
assertEquals(Unit.PINCH, pinch.getUnit());
|
Ingredient ingredient = new Ingredient("Bread", 1, 2, 3);
|
||||||
assertEquals(Unit.HANDFUL, handful.getUnit());
|
FormalIngredient fi = new FormalIngredient(ingredient, 1.0, "g");
|
||||||
assertEquals(Unit.TO_TASTE, toTaste.getUnit());
|
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
|
@Test
|
||||||
void getUnknownUnitTest(){
|
void testFormalIngredientScaleByFactor() {
|
||||||
assertNull(invalid.getUnit());
|
ingredientUnitMap.replaceAll(
|
||||||
}
|
(_, b) -> b.scaleBy(2)
|
||||||
|
);
|
||||||
@Test
|
// Each amount is doubled after the mapping
|
||||||
void convertFormalToBaseUnit(){
|
assertEquals(ingredientUnitMap.size(), ingredientUnitMap.values().stream()
|
||||||
assertEquals(1000, kilogram.amountInBaseUnit());
|
.filter(i -> i.getAmount() == 2.0).count());
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
|
@ -12,6 +13,17 @@ class RecipeTest {
|
||||||
|
|
||||||
Recipe recipe;
|
Recipe recipe;
|
||||||
static final Long RECIPE_ID = 1L;
|
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
|
@BeforeEach
|
||||||
void setupRecipe() {
|
void setupRecipe() {
|
||||||
|
|
@ -42,8 +54,7 @@ class RecipeTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getIngredientsAddThrow() {
|
void getIngredientsAddThrow() {
|
||||||
// TODO: Change to actual Ingredient class later
|
assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().add(testIngredient));
|
||||||
assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().add("Lasagna"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -53,8 +64,10 @@ class RecipeTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void setIngredients() {
|
void setIngredients() {
|
||||||
// TODO: Change to actual Ingredient class later
|
List<RecipeIngredient> ingredients =
|
||||||
List<String> ingredients = new ArrayList<>(List.of("Chocolate", "Flour", "Egg"));
|
Stream.of("Chocolate", "Flour", "Egg")
|
||||||
|
.map(RecipeTest::getTemplate)
|
||||||
|
.toList();
|
||||||
recipe.setIngredients(ingredients);
|
recipe.setIngredients(ingredients);
|
||||||
|
|
||||||
assertEquals(recipe.getIngredients(), ingredients);
|
assertEquals(recipe.getIngredients(), ingredients);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package commons;
|
package commons;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
|
@ -11,64 +12,55 @@ class UnitTest {
|
||||||
private static final double KILOGRAMS = 1000.0;
|
private static final double KILOGRAMS = 1000.0;
|
||||||
private static final double MILLILITERS = 1.0;
|
private static final double MILLILITERS = 1.0;
|
||||||
private static final double LITERS = 1000.0;
|
private static final double LITERS = 1000.0;
|
||||||
private static final double TABLESPOONS = 15.0;
|
private static final double TABLESPOONS = 14.0;
|
||||||
private static final double TEASPOONS = 5.0;
|
private static final double CUPS = 225.0;
|
||||||
private static final double CUPS = 240.0;
|
|
||||||
private static final double PIECES = 1.0;
|
|
||||||
|
|
||||||
private static final double NoMeaningfulValue = 0.0;
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void formalUnitMarkedFormal(){
|
void formalUnitMarkedFormal(){
|
||||||
assertTrue(Unit.GRAM.isFormal());
|
assertTrue(Unit.GRAMME.isFormal());
|
||||||
assertTrue(Unit.KILOGRAM.isFormal());
|
assertTrue(Unit.KILOGRAMME.isFormal());
|
||||||
assertTrue(Unit.LITER.isFormal());
|
assertTrue(Unit.LITRE.isFormal());
|
||||||
assertTrue(Unit.CUP.isFormal());
|
assertTrue(Unit.CUP.isFormal());
|
||||||
assertTrue(Unit.MILLILITER.isFormal());
|
assertTrue(Unit.MILLILITRE.isFormal());
|
||||||
assertTrue(Unit.PIECE.isFormal());
|
|
||||||
assertTrue(Unit.TABLESPOON.isFormal());
|
assertTrue(Unit.TABLESPOON.isFormal());
|
||||||
assertTrue(Unit.TEASPOON.isFormal());
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void informalUnitAreNotFormal() {
|
void informalUnitAreNotFormal() {
|
||||||
assertFalse(Unit.PINCH.isFormal());
|
assertFalse(Unit.INFORMAL.isFormal());
|
||||||
assertFalse(Unit.HANDFUL.isFormal());
|
|
||||||
assertFalse(Unit.TO_TASTE.isFormal());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void conversionIsCorrect() {
|
void conversionIsCorrect() {
|
||||||
assertEquals(GRAMS, Unit.GRAM.conversionFactor);
|
assertEquals(GRAMS, Unit.GRAMME.conversionFactor);
|
||||||
assertEquals(KILOGRAMS, Unit.KILOGRAM.conversionFactor);
|
assertEquals(KILOGRAMS, Unit.KILOGRAMME.conversionFactor);
|
||||||
assertEquals(LITERS, Unit.LITER.conversionFactor);
|
assertEquals(LITERS, Unit.LITRE.conversionFactor);
|
||||||
assertEquals(CUPS, Unit.CUP.conversionFactor);
|
assertEquals(CUPS, Unit.CUP.conversionFactor);
|
||||||
assertEquals(MILLILITERS, Unit.MILLILITER.conversionFactor);
|
assertEquals(MILLILITERS, Unit.MILLILITRE.conversionFactor);
|
||||||
assertEquals(PIECES, Unit.PIECE.conversionFactor);
|
|
||||||
assertEquals(TABLESPOONS, Unit.TABLESPOON.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
|
@Test
|
||||||
void toStringReturnsName(){
|
void toStringReturnsName(){
|
||||||
assertEquals("GRAM", Unit.GRAM.toString());
|
assertEquals("g", Unit.GRAMME.toString());
|
||||||
assertEquals("KILOGRAM", Unit.KILOGRAM.toString());
|
assertEquals("kg", Unit.KILOGRAMME.toString());
|
||||||
assertEquals("LITER", Unit.LITER.toString());
|
assertEquals("l", Unit.LITRE.toString());
|
||||||
assertEquals("CUP", Unit.CUP.toString());
|
assertEquals("cup(s)", Unit.CUP.toString());
|
||||||
assertEquals("MILLILITER", Unit.MILLILITER.toString());
|
assertEquals("ml", Unit.MILLILITRE.toString());
|
||||||
assertEquals("PIECE", Unit.PIECE.toString());
|
assertEquals("tbsp", Unit.TABLESPOON.toString());
|
||||||
assertEquals("TABLESPOON", Unit.TABLESPOON.toString());
|
}
|
||||||
assertEquals("TEASPOON", Unit.TEASPOON.toString());
|
@Test
|
||||||
|
void testFromSuffixCreatesSomeUnit() {
|
||||||
assertEquals("PINCH", Unit.PINCH.toString());
|
assertTrue(Unit.fromString("g").isPresent());
|
||||||
assertEquals("HANDFUL", Unit.HANDFUL.toString());
|
}
|
||||||
assertEquals("TO_TASTE", Unit.TO_TASTE.toString());
|
@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();
|
Ingredient example = new Ingredient();
|
||||||
example.name = ingredient.name;
|
example.name = ingredient.name;
|
||||||
|
|
||||||
if (ingredientRepository.exists(Example.of(example))) {
|
if (ingredientRepository.existsById(ingredient.id) || ingredientRepository.exists(Example.of(example))) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package server.api;
|
package server.api;
|
||||||
|
|
||||||
|
import commons.Ingredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
|
|
||||||
import commons.ws.Topics;
|
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.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import server.database.IngredientRepository;
|
||||||
|
import server.database.RecipeIngredientRepository;
|
||||||
import server.database.RecipeRepository;
|
import server.database.RecipeRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -31,11 +34,17 @@ import java.util.Optional;
|
||||||
public class RecipeController {
|
public class RecipeController {
|
||||||
private final RecipeRepository recipeRepository; // JPA repository used in this controller
|
private final RecipeRepository recipeRepository; // JPA repository used in this controller
|
||||||
private final SimpMessagingTemplate messagingTemplate;
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final RecipeIngredientRepository recipeIngredientRepository;
|
||||||
|
private final IngredientRepository ingredientRepository;
|
||||||
|
|
||||||
public RecipeController(RecipeRepository recipeRepository,
|
public RecipeController(RecipeRepository recipeRepository,
|
||||||
SimpMessagingTemplate messagingTemplate) {
|
SimpMessagingTemplate messagingTemplate,
|
||||||
|
IngredientRepository ingredientRepository,
|
||||||
|
RecipeIngredientRepository recipeIngredientRepository) {
|
||||||
this.recipeRepository = recipeRepository;
|
this.recipeRepository = recipeRepository;
|
||||||
this.messagingTemplate = messagingTemplate;
|
this.messagingTemplate = messagingTemplate;
|
||||||
|
this.recipeIngredientRepository = recipeIngredientRepository;
|
||||||
|
this.ingredientRepository = ingredientRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -72,20 +81,33 @@ public class RecipeController {
|
||||||
|
|
||||||
return ResponseEntity.ok(recipeRepository.findAll());
|
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>.
|
* 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 id The recipe id to replace
|
||||||
* @param recipe The new recipe to be replaced from the original
|
* @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
|
* @return The changed recipe; returns 400 Bad Request if the recipe does not exist
|
||||||
|
* @see IngredientController#createIngredient(Ingredient)
|
||||||
*/
|
*/
|
||||||
@PostMapping("/recipe/{id}")
|
@PostMapping("/recipe/{id}")
|
||||||
public ResponseEntity<Recipe> updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) {
|
public ResponseEntity<Recipe> updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) {
|
||||||
if (!recipeRepository.existsById(id)) {
|
if (!recipeRepository.existsById(id)) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
Recipe saved = saveRecipeAndDependencies(recipe);
|
||||||
Recipe saved = recipeRepository.save(recipe);
|
|
||||||
messagingTemplate.convertAndSend(Topics.RECIPES, new UpdateRecipeMessage(saved));
|
messagingTemplate.convertAndSend(Topics.RECIPES, new UpdateRecipeMessage(saved));
|
||||||
|
|
||||||
return ResponseEntity.ok(saved);
|
return ResponseEntity.ok(saved);
|
||||||
|
|
@ -93,11 +115,13 @@ public class RecipeController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping for <code>PUT /recipe/new</code>.
|
* Mapping for <code>PUT /recipe/new</code>.
|
||||||
|
* Includes same transient object handling as the POST handler.
|
||||||
* <p>
|
* <p>
|
||||||
* Inserts a new recipe into the repository
|
* Inserts a new recipe into the repository
|
||||||
* </p>
|
* </p>
|
||||||
* @param recipe The new recipe as a request body
|
* @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
|
* @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")
|
@PutMapping("/recipe/new")
|
||||||
public ResponseEntity<Recipe> createRecipe(@RequestBody Recipe recipe) {
|
public ResponseEntity<Recipe> createRecipe(@RequestBody Recipe recipe) {
|
||||||
|
|
@ -113,8 +137,7 @@ public class RecipeController {
|
||||||
if (recipeRepository.exists(Example.of(example))) {
|
if (recipeRepository.exists(Example.of(example))) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
Recipe saved = saveRecipeAndDependencies(recipe);
|
||||||
Recipe saved = recipeRepository.save(recipe);
|
|
||||||
messagingTemplate.convertAndSend(Topics.RECIPES, new CreateRecipeMessage(saved));
|
messagingTemplate.convertAndSend(Topics.RECIPES, new CreateRecipeMessage(saved));
|
||||||
|
|
||||||
return ResponseEntity.ok(saved);
|
return ResponseEntity.ok(saved);
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface IngredientRepository extends JpaRepository<Ingredient, Long> {
|
public interface IngredientRepository extends JpaRepository<Ingredient, Long> {
|
||||||
List<Ingredient> findAllByOrderByNameAsc();
|
List<Ingredient> findAllByOrderByNameAsc();
|
||||||
Page<Ingredient> findAllByOrderByNameAsc(Pageable pageable);
|
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.messaging.simp.SimpMessagingTemplate;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
import server.WebSocketConfig;
|
import server.WebSocketConfig;
|
||||||
|
import server.database.IngredientRepository;
|
||||||
|
import server.database.RecipeIngredientRepository;
|
||||||
import server.database.RecipeRepository;
|
import server.database.RecipeRepository;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -45,12 +47,21 @@ public class RecipeControllerTest {
|
||||||
private final RecipeRepository recipeRepository;
|
private final RecipeRepository recipeRepository;
|
||||||
private List<Long> recipeIds;
|
private List<Long> recipeIds;
|
||||||
public static final int NUM_RECIPES = 10;
|
public static final int NUM_RECIPES = 10;
|
||||||
|
private final IngredientRepository ingredientRepository;
|
||||||
|
private final RecipeIngredientRepository recipeIngredientRepository;
|
||||||
|
|
||||||
// Injects a test repository into the test class
|
// Injects a test repository into the test class
|
||||||
@Autowired
|
@Autowired
|
||||||
public RecipeControllerTest(RecipeRepository recipeRepository, SimpMessagingTemplate template) {
|
public RecipeControllerTest(
|
||||||
|
RecipeRepository recipeRepository,
|
||||||
|
SimpMessagingTemplate template,
|
||||||
|
IngredientRepository ingredientRepository,
|
||||||
|
RecipeIngredientRepository recipeIngredientRepository
|
||||||
|
) {
|
||||||
this.recipeRepository = recipeRepository;
|
this.recipeRepository = recipeRepository;
|
||||||
this.template = template;
|
this.template = template;
|
||||||
|
this.ingredientRepository = ingredientRepository;
|
||||||
|
this.recipeIngredientRepository = recipeIngredientRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
|
@ -59,7 +70,12 @@ public class RecipeControllerTest {
|
||||||
.range(0, NUM_RECIPES)
|
.range(0, NUM_RECIPES)
|
||||||
.mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of()))
|
.mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of()))
|
||||||
.toList();
|
.toList();
|
||||||
controller = new RecipeController(recipeRepository, template);
|
controller = new RecipeController(
|
||||||
|
recipeRepository,
|
||||||
|
template,
|
||||||
|
ingredientRepository,
|
||||||
|
recipeIngredientRepository
|
||||||
|
);
|
||||||
Set<String> tags = info.getTags();
|
Set<String> tags = info.getTags();
|
||||||
List<Long> ids = new ArrayList<>();
|
List<Long> ids = new ArrayList<>();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue