refactor: newest delivery from main

This commit is contained in:
Zhongheng Liu 2025-12-19 23:54:45 +02:00
commit 00e4bb03f4
Signed by: steven
GPG key ID: F69B980899C1C09D
32 changed files with 722 additions and 881 deletions

View file

@ -34,21 +34,25 @@
<artifactId>jersey-client</artifactId> <artifactId>jersey-client</artifactId>
<version>${version.jersey}</version> <version>${version.jersey}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.glassfish.jersey.inject</groupId> <groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId> <artifactId>jersey-hk2</artifactId>
<version>${version.jersey}</version> <version>${version.jersey}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.glassfish.jersey.media</groupId> <groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId> <artifactId>jersey-media-json-jackson</artifactId>
<version>${version.jersey}</version> <version>${version.jersey}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>jakarta.activation</groupId> <groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId> <artifactId>jakarta.activation-api</artifactId>
<version>2.1.3</version> <version>2.1.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.inject</groupId> <groupId>com.google.inject</groupId>
<artifactId>guice</artifactId> <artifactId>guice</artifactId>
@ -61,11 +65,13 @@
<artifactId>javafx-fxml</artifactId> <artifactId>javafx-fxml</artifactId>
<version>${version.jfx}</version> <version>${version.jfx}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<version>${version.jfx}</version> <version>${version.jfx}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId> <artifactId>javafx-web</artifactId>
@ -91,6 +97,27 @@
<version>${version.mockito}</version> <version>${version.mockito}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- websockets -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>6.2.12</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.tyrus.bundles/tyrus-standalone-client -->
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>6.2.12</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -18,13 +18,18 @@ package client;
import client.scenes.FoodpalApplicationCtrl; import client.scenes.FoodpalApplicationCtrl;
import client.scenes.recipe.IngredientListCtrl; import client.scenes.recipe.IngredientListCtrl;
import client.scenes.recipe.RecipeStepListCtrl; import client.scenes.recipe.RecipeStepListCtrl;
import client.utils.ConfigService;
import client.utils.LocaleManager; import client.utils.LocaleManager;
import client.utils.ServerUtils;
import client.utils.WebSocketUtils;
import com.google.inject.Binder; import com.google.inject.Binder;
import com.google.inject.Module; import com.google.inject.Module;
import com.google.inject.Scopes; import com.google.inject.Scopes;
import client.scenes.MainCtrl; import client.scenes.MainCtrl;
import java.nio.file.Path;
public class MyModule implements Module { public class MyModule implements Module {
@Override @Override
@ -33,6 +38,12 @@ public class MyModule implements Module {
binder.bind(FoodpalApplicationCtrl.class).in(Scopes.SINGLETON); binder.bind(FoodpalApplicationCtrl.class).in(Scopes.SINGLETON);
binder.bind(IngredientListCtrl.class).in(Scopes.SINGLETON); binder.bind(IngredientListCtrl.class).in(Scopes.SINGLETON);
binder.bind(RecipeStepListCtrl.class).in(Scopes.SINGLETON); binder.bind(RecipeStepListCtrl.class).in(Scopes.SINGLETON);
binder.bind(LocaleManager.class).in(Scopes.SINGLETON); binder.bind(LocaleManager.class).in(Scopes.SINGLETON);
binder.bind(ServerUtils.class).in(Scopes.SINGLETON);
binder.bind(WebSocketUtils.class).in(Scopes.SINGLETON);
binder.bind(ConfigService.class).toInstance(new ConfigService(Path.of("config.json")));
} }
} }

View file

@ -9,17 +9,22 @@ import client.exception.UpdateException;
import client.scenes.recipe.IngredientListCtrl; import client.scenes.recipe.IngredientListCtrl;
import client.scenes.recipe.RecipeStepListCtrl; import client.scenes.recipe.RecipeStepListCtrl;
import client.utils.Config;
import client.utils.ConfigService;
import client.utils.DefaultRecipeFactory; import client.utils.DefaultRecipeFactory;
import client.utils.LocaleAware; import client.utils.LocaleAware;
import client.utils.LocaleManager; import client.utils.LocaleManager;
import client.utils.ServerUtils; import client.utils.ServerUtils;
import client.utils.WebSocketUtils;
import commons.Recipe; import commons.Recipe;
import commons.ws.Topics;
import commons.ws.messages.Message;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
@ -31,6 +36,7 @@ import javafx.scene.text.Font;
public class FoodpalApplicationCtrl implements LocaleAware { public class FoodpalApplicationCtrl implements LocaleAware {
private final ServerUtils server; private final ServerUtils server;
private final WebSocketUtils webSocketUtils;
private final LocaleManager localeManager; private final LocaleManager localeManager;
private final IngredientListCtrl ingredientListCtrl; private final IngredientListCtrl ingredientListCtrl;
private final RecipeStepListCtrl stepListCtrl; private final RecipeStepListCtrl stepListCtrl;
@ -41,10 +47,9 @@ public class FoodpalApplicationCtrl implements LocaleAware {
// everything in the left lane // everything in the left lane
@FXML @FXML
public Label recipesLabel; public Label recipesLabel;
public ComboBox<String> langSelectMenu;
@FXML @FXML
private ListView<Recipe> recipeList; public ListView<Recipe> recipeList;
@FXML @FXML
private Button addRecipeButton; private Button addRecipeButton;
@ -55,6 +60,13 @@ public class FoodpalApplicationCtrl implements LocaleAware {
@FXML @FXML
public Button cloneRecipeButton; public Button cloneRecipeButton;
@FXML
private Button favouriteButton;
private Config config;
private final ConfigService configService;
// === CENTER: RECIPE DETAILS === // === CENTER: RECIPE DETAILS ===
@FXML @FXML
@ -69,14 +81,20 @@ public class FoodpalApplicationCtrl implements LocaleAware {
@Inject @Inject
public FoodpalApplicationCtrl( public FoodpalApplicationCtrl(
ServerUtils server, ServerUtils server,
WebSocketUtils webSocketUtils,
LocaleManager localeManager, LocaleManager localeManager,
IngredientListCtrl ingredientListCtrl, IngredientListCtrl ingredientListCtrl,
RecipeStepListCtrl stepListCtrl RecipeStepListCtrl stepListCtrl,
ConfigService configService
) { ) {
this.server = server; this.server = server;
this.webSocketUtils = webSocketUtils;
this.localeManager = localeManager; this.localeManager = localeManager;
this.ingredientListCtrl = ingredientListCtrl; this.ingredientListCtrl = ingredientListCtrl;
this.stepListCtrl = stepListCtrl; this.stepListCtrl = stepListCtrl;
this.configService = configService;
initializeWebSocket();
} }
private void initRecipeList() { private void initRecipeList() {
// Show recipe name in the list // Show recipe name in the list
@ -87,10 +105,12 @@ public class FoodpalApplicationCtrl implements LocaleAware {
setText(empty || item == null ? "" : item.getName()); setText(empty || item == null ? "" : item.getName());
} }
}); });
// When your selection changes, update details in the panel // When your selection changes, update details in the panel
recipeList.getSelectionModel().selectedItemProperty().addListener( recipeList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldRecipe, newRecipe) -> showRecipeDetails(newRecipe) (obs, oldRecipe, newRecipe) -> {
showRecipeDetails(newRecipe);
updateFavouriteButton(newRecipe);
}
); );
// Double-click to go to detail screen // Double-click to go to detail screen
@ -101,6 +121,27 @@ public class FoodpalApplicationCtrl implements LocaleAware {
} }
}); });
} }
private void initializeWebSocket() {
webSocketUtils.connect(() -> {
webSocketUtils.subscribe(Topics.RECIPES, (Message _) -> {
Platform.runLater(() -> {
Recipe selectedRecipe = recipeList.getSelectionModel().getSelectedItem();
refresh(); // refresh the left list
if (selectedRecipe == null) {
return;
}
// select last selected recipe if it still exists, first otherwise (done by refresh())
Recipe recipeInList = recipeList.getItems().stream()
.filter(r -> r.getId().equals(selectedRecipe.getId()))
.findFirst()
.orElse(null);
showRecipeDetails(recipeInList);
}); // runLater as it's on another non-FX thread.
});
});
}
private void initStepsIngredientsList() { private void initStepsIngredientsList() {
// Initialize callback for ingredient list updates // Initialize callback for ingredient list updates
this.ingredientListCtrl.setUpdateCallback(newList -> { this.ingredientListCtrl.setUpdateCallback(newList -> {
@ -134,12 +175,14 @@ public class FoodpalApplicationCtrl implements LocaleAware {
} }
@Override @Override
public void initializeComponents() { public void initializeComponents() {
// TODO Reduce code duplication?? config = configService.getConfig();
initStepsIngredientsList(); initStepsIngredientsList();
initRecipeList(); initRecipeList();
refresh(); refresh();
updateFavouriteButton(recipeList.getSelectionModel().getSelectedItem());
} }
private void showName(String name) { private void showName(String name) {
final int NAME_FONT_SIZE = 20; final int NAME_FONT_SIZE = 20;
editableTitleArea.getChildren().clear(); editableTitleArea.getChildren().clear();
@ -147,6 +190,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
nameLabel.setFont(new Font("System Bold", NAME_FONT_SIZE)); nameLabel.setFont(new Font("System Bold", NAME_FONT_SIZE));
editableTitleArea.getChildren().add(nameLabel); editableTitleArea.getChildren().add(nameLabel);
} }
@Override @Override
public void updateText() { public void updateText() {
addRecipeButton.setText(getLocaleString("menu.button.add.recipe")); addRecipeButton.setText(getLocaleString("menu.button.add.recipe"));
@ -158,13 +202,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
printRecipeButton.setText(getLocaleString("menu.button.print")); printRecipeButton.setText(getLocaleString("menu.button.print"));
recipesLabel.setText(getLocaleString("menu.label.recipes")); recipesLabel.setText(getLocaleString("menu.label.recipes"));
// TODO propagate ResourceBundle lang changes to nested controller
// ingredientsLabel.setText(getLocaleString("menu.label.ingredients"));
// preparationLabel.setText(getLocaleString("menu.label.preparation"));
// addIngredientButton.setText(getLocaleString("menu.button.add.ingredient"));
// addPreparationStepButton.setText(getLocaleString("menu.button.add.step"));
} }
@Override @Override
@ -200,11 +237,13 @@ public class FoodpalApplicationCtrl implements LocaleAware {
} }
detailsScreen.visibleProperty().set(!recipes.isEmpty()); detailsScreen.visibleProperty().set(!recipes.isEmpty());
} }
private void printError(String msg) { private void printError(String msg) {
Alert alert = new Alert(Alert.AlertType.ERROR); Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setContentText(msg); alert.setContentText(msg);
alert.showAndWait(); alert.showAndWait();
} }
/** /**
* Adds a recipe, by providing a default name "Untitled recipe (n)". * Adds a recipe, by providing a default name "Untitled recipe (n)".
* The UX flow is now: * The UX flow is now:
@ -277,7 +316,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
try { try {
server.updateRecipe(selected); server.updateRecipe(selected);
refresh(); refresh();
recipeList.getSelectionModel().select(selected); //recipeList.getSelectionModel().select(selected);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// throw a nice blanket UpdateException // throw a nice blanket UpdateException
throw new UpdateException("Error occurred when updating recipe name!"); throw new UpdateException("Error occurred when updating recipe name!");
@ -287,7 +326,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
editableTitleArea.getChildren().add(edit); editableTitleArea.getChildren().add(edit);
edit.requestFocus(); edit.requestFocus();
} }
@FXML @FXML
private void makePrintable() { private void makePrintable() {
System.out.println("Recipe printed"); System.out.println("Recipe printed");
@ -307,6 +345,39 @@ public class FoodpalApplicationCtrl implements LocaleAware {
recipeList.getSelectionModel().select(cloned); recipeList.getSelectionModel().select(cloned);
} }
private void updateFavouriteButton(Recipe recipe) {
if (recipe == null) {
favouriteButton.setDisable(true);
favouriteButton.setText("");
return;
}
favouriteButton.setDisable(false);
favouriteButton.setText(
config.isFavourite(recipe.getId()) ? "" : ""
);
}
@FXML
private void toggleFavourite() {
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
if (selected == null) {
return;
}
long id = selected.getId();
if (config.isFavourite(id)) {
config.removeFavourite(id);
} else {
config.addFavourite(id);
}
configService.save();
updateFavouriteButton(selected);
}
} }

View file

@ -0,0 +1,38 @@
package client.scenes.nutrition;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import com.google.inject.Inject;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
public class NutritionDetailsCtrl implements LocaleAware {
private final LocaleManager manager;
public Label ingredientName;
public Label fatInputLabel;
public Label proteinInputLabel;
public Label carbInputLabel;
public Label estimatedKcalLabel;
public Label usageLabel;
public TextField fatInputElement;
public TextField proteinInputElement;
public TextField carbInputElement;
@Inject
public NutritionDetailsCtrl(
LocaleManager manager
) {
this.manager = manager;
}
@Override
public void updateText() {
}
@Override
public LocaleManager getLocaleManager() {
return manager;
}
}

View file

@ -0,0 +1,68 @@
package client.scenes.nutrition;
import client.scenes.FoodpalApplicationCtrl;
import com.google.inject.Inject;
import commons.Recipe;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.ListView;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NutritionViewCtrl {
private ObservableList<Recipe> recipes;
private HashMap<String, Integer> ingredientStats;
public ListView<String> nutritionIngredientsView;
private final NutritionDetailsCtrl nutritionDetailsCtrl;
// TODO into Ingredient class definition
/**
* Comedically verbose function to count unique appearances of an ingredient by name in each recipe.
* For each recipe:
* 1. Collect unique ingredients that appeared in that recipe.
* 2. For each unique ingredient in said recipe:
* 1. Initialize the appearance for that ingredient to 0.
* 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
*/
private void updateIngredientStats(
List<Recipe> recipeList
) {
recipeList.forEach(recipe -> {
Set<String> uniqueIngredients = new HashSet<>(recipe.getIngredients());
nutritionIngredientsView.getItems().setAll(uniqueIngredients);
uniqueIngredients.forEach(ingredient -> {
ingredientStats.put(ingredient, 0);
recipeList.forEach(r ->
ingredientStats.put(
ingredient,
ingredientStats.get(ingredient) + (
(r.getIngredients().contains(ingredient))
? 1 : 0
)
)
);
});
});
}
@Inject
public NutritionViewCtrl(
FoodpalApplicationCtrl foodpalApplicationCtrl,
NutritionDetailsCtrl nutritionDetailsCtrl
) {
this.recipes = foodpalApplicationCtrl.recipeList.getItems();
this.recipes.addListener((ListChangeListener<? super Recipe>) _ -> {
updateIngredientStats(this.recipes);
});
this.nutritionDetailsCtrl = nutritionDetailsCtrl;
this.nutritionIngredientsView.selectionModelProperty().addListener((observable, oldValue, newValue) -> {
});
}
}

View file

@ -12,7 +12,8 @@ public class Config {
private List<Long> favourites = new ArrayList<>(); private List<Long> favourites = new ArrayList<>();
private List<String> shoppingList = new ArrayList<>(); private List<String> shoppingList = new ArrayList<>();
public Config(){} public Config() {
}
public String getLanguage() { public String getLanguage() {
return language; return language;
@ -22,10 +23,6 @@ public class Config {
return shoppingList; return shoppingList;
} }
public List<Long> getFavourites() {
return favourites;
}
public String getServerUrl() { public String getServerUrl() {
return serverUrl; return serverUrl;
} }
@ -45,4 +42,29 @@ public class Config {
public void setShoppingList(List<String> shoppingList) { public void setShoppingList(List<String> shoppingList) {
this.shoppingList = shoppingList; this.shoppingList = shoppingList;
} }
// favourite helper
public List<Long> getFavourites() {
if (favourites == null) {
favourites = new ArrayList<>();
} }
return favourites;
}
// to avoid null pointers.
public boolean isFavourite(long recipeId) {
return getFavourites().contains(recipeId);
}
public void addFavourite(long recipeId) {
if (!getFavourites().contains(recipeId)) {
getFavourites().add(recipeId);
}
}
public void removeFavourite(long recipeId) {
getFavourites().remove(recipeId);
}
}

View file

@ -1,72 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client.utils;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import java.net.ConnectException;
import java.util.List;
import jakarta.ws.rs.core.GenericType;
import org.glassfish.jersey.client.ClientConfig;
import commons.Quote;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
public class ServerUtilsExample {
private static final String SERVER = "http://localhost:8080/";
// public void getQuotesTheHardWay() throws IOException, URISyntaxException {
// var url = new URI("http://localhost:8080/api/quotes").toURL();
// var is = url.openConnection().getInputStream();
// var br = new BufferedReader(new InputStreamReader(is));
// String line;
// while ((line = br.readLine()) != null) {
// System.out.println(line);
// }
// }
public List<Quote> getQuotes() {
return ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER).path("api/quotes") //
.request(APPLICATION_JSON) //
.get(new GenericType<List<Quote>>() {});
}
public Quote addQuote(Quote quote) {
return ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER).path("api/quotes") //
.request(APPLICATION_JSON) //
.post(Entity.entity(quote, APPLICATION_JSON), Quote.class);
}
public boolean isServerAvailable() {
try {
ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER) //
.request(APPLICATION_JSON) //
.get();
} catch (ProcessingException e) {
if (e.getCause() instanceof ConnectException) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,111 @@
package client.utils;
import commons.ws.messages.Message;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import java.lang.reflect.Type;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
public class WebSocketUtils {
private static final String WS_URL = "ws://localhost:8080/updates";
private WebSocketStompClient stompClient;
private StompSession stompSession;
/**
* Connect to the websocket server.
* @param onConnected OnConnected callback
*/
public void connect(@Nullable Runnable onConnected) {
StandardWebSocketClient webSocketClient = new StandardWebSocketClient(); // Create WS Client
stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new MappingJackson2MessageConverter()); // Use jackson
CompletableFuture<StompSession> sessionFuture = stompClient.connectAsync(
WS_URL,
new StompSessionHandlerAdapter() {
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
stompSession = session;
System.out.println("WebSocket connected: " + session.getSessionId());
}
@Override
public void handleException(StompSession session, @Nullable StompCommand command,
StompHeaders headers, byte[] payload,
Throwable exception) {
System.err.println("STOMP error: " + exception.getMessage());
exception.printStackTrace();
}
@Override
public void handleTransportError(StompSession session, Throwable exception) {
System.err.println("STOMP transport error: " + exception.getMessage());
exception.printStackTrace();
}
}
);
sessionFuture.thenAccept(session -> {
stompSession = session;
System.out.println("Connection successful, session ready");
if (onConnected != null) {
onConnected.run();
}
}).exceptionally(throwable -> {
System.err.println("Failed to connect: " + throwable.getMessage());
throwable.printStackTrace();
return null;
});
}
/**
* Subscribe to a topic.
* @param destination Destination to subscribe to, for example: /subscribe/recipe
* @param messageHandler Handler for received messages
*/
public void subscribe(String destination, Consumer<Message> messageHandler) {
if (stompSession == null || !stompSession.isConnected()) {
System.err.println("Cannot subscribe - not connected");
return;
}
stompSession.subscribe(destination, new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return Message.class;
}
@Override
public void handleFrame(StompHeaders headers, @Nullable Object payload) {
Message message = (Message) payload;
messageHandler.accept(message);
}
});
System.out.println("Subscribed to: " + destination);
}
public void disconnect() {
if (stompSession != null && stompSession.isConnected()) {
stompSession.disconnect();
}
if (stompClient != null) {
stompClient.stop();
}
}
public boolean isConnected() {
return stompSession != null && stompSession.isConnected();
}
}

View file

@ -69,6 +69,7 @@
<Button fx:id="editRecipeTitleButton" onAction="#editRecipeTitle" text="Edit" /> <Button fx:id="editRecipeTitleButton" onAction="#editRecipeTitle" text="Edit" />
<Button fx:id="removeRecipeButton2" mnemonicParsing="false" onAction="#removeSelectedRecipe" text="Remove Recipe" /> <Button fx:id="removeRecipeButton2" mnemonicParsing="false" onAction="#removeSelectedRecipe" text="Remove Recipe" />
<Button fx:id="printRecipeButton" mnemonicParsing="false" onAction="#makePrintable" text="Print Recipe" /> <Button fx:id="printRecipeButton" mnemonicParsing="false" onAction="#makePrintable" text="Print Recipe" />
<Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" />
</HBox> </HBox>
<!-- Ingredients --> <!-- Ingredients -->

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.shape.Line?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="client.scenes.nutrition.NutritionDetailsCtrl"
prefHeight="400.0" prefWidth="600.0">
<VBox visible="false">
<Label fx:id="ingredientName" />
<HBox>
<Label fx:id="fatInputLabel">Fat: </Label>
<TextField fx:id="fatInputElement" />
</HBox>
<HBox>
<Label fx:id="proteinInputLabel">Protein: </Label>
<TextField fx:id="proteinInputElement" />
</HBox>
<HBox>
<Label fx:id="carbInputLabel">Carbohydrates: </Label>
<TextField fx:id="carbInputElement" />
</HBox>
<Label fx:id="estimatedKcalLabel">Estimated: 0kcal</Label>
<Label fx:id="usageLabel">Not used in any recipes</Label>
</VBox>
</AnchorPane>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="client.scenes.nutrition.NutritionViewCtrl"
prefHeight="400.0" prefWidth="600.0">
<SplitPane>
<ListView fx:id="nutritionIngredientsView" />
<AnchorPane>
<fx:include source="NutritionDetails.fxml" />
</AnchorPane>
</SplitPane>
</AnchorPane>

View file

@ -45,6 +45,12 @@
<version>${version.mockito}</version> <version>${version.mockito}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.20.1</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>

View file

@ -1,63 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package commons;
import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long id;
public String firstName;
public String lastName;
@SuppressWarnings("unused")
private Person() {
// for object mapper
}
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, MULTI_LINE_STYLE);
}
}

View file

@ -1,66 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package commons;
import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
@Entity
public class Quote {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long id;
@OneToOne(cascade = CascadeType.PERSIST)
public Person person;
public String quote;
@SuppressWarnings("unused")
private Quote() {
// for object mappers
}
public Quote(Person person, String quote) {
this.person = person;
this.quote = quote;
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, MULTI_LINE_STYLE);
}
}

View file

@ -10,6 +10,8 @@ import commons.Ingredient;
public class CreateIngredientMessage implements Message { public class CreateIngredientMessage implements Message {
private Ingredient ingredient; private Ingredient ingredient;
public CreateIngredientMessage() {} // for jackson
public CreateIngredientMessage(Ingredient ingredient) { public CreateIngredientMessage(Ingredient ingredient) {
this.ingredient = ingredient; this.ingredient = ingredient;
} }
@ -27,4 +29,9 @@ public class CreateIngredientMessage implements Message {
public Ingredient getIngredient() { public Ingredient getIngredient() {
return ingredient; return ingredient;
} }
// for jackson
public void setIngredient(Ingredient ingredient) {
this.ingredient = ingredient;
}
} }

View file

@ -10,6 +10,8 @@ import commons.Recipe;
public class CreateRecipeMessage implements Message { public class CreateRecipeMessage implements Message {
private Recipe recipe; private Recipe recipe;
public CreateRecipeMessage() {} // for jackson
public CreateRecipeMessage(Recipe recipe) { public CreateRecipeMessage(Recipe recipe) {
this.recipe = recipe; this.recipe = recipe;
} }
@ -27,4 +29,9 @@ public class CreateRecipeMessage implements Message {
public Recipe getRecipe() { public Recipe getRecipe() {
return recipe; return recipe;
} }
// for jackson
public void setRecipe(Recipe recipe) {
this.recipe = recipe;
}
} }

View file

@ -8,6 +8,8 @@ package commons.ws.messages;
public class DeleteIngredientMessage implements Message { public class DeleteIngredientMessage implements Message {
private Long ingredientId; private Long ingredientId;
public DeleteIngredientMessage() {} // for jackson
public DeleteIngredientMessage(Long ingredientId) { public DeleteIngredientMessage(Long ingredientId) {
this.ingredientId = ingredientId; this.ingredientId = ingredientId;
} }
@ -25,4 +27,9 @@ public class DeleteIngredientMessage implements Message {
public Long getIngredientId() { public Long getIngredientId() {
return ingredientId; return ingredientId;
} }
// for jackson
public void setIngredientId(Long ingredientId) {
this.ingredientId = ingredientId;
}
} }

View file

@ -8,6 +8,8 @@ package commons.ws.messages;
public class DeleteRecipeMessage implements Message { public class DeleteRecipeMessage implements Message {
private Long recipeId; private Long recipeId;
public DeleteRecipeMessage() {} // for jackson
public DeleteRecipeMessage(Long recipeId) { public DeleteRecipeMessage(Long recipeId) {
this.recipeId = recipeId; this.recipeId = recipeId;
} }
@ -25,4 +27,9 @@ public class DeleteRecipeMessage implements Message {
public Long getRecipeId() { public Long getRecipeId() {
return recipeId; return recipeId;
} }
// for jackson
public void setRecipeId(Long recipeId) {
this.recipeId = recipeId;
}
} }

View file

@ -0,0 +1,28 @@
package commons.ws.messages;
/**
* Message sent when a recipe becomes a favourite.
* @see commons.ws.messages.Message.Type#RECIPE_FAVOURITE
*/
public class FavouriteRecipeMessage implements Message{
private Long recipeId;
public FavouriteRecipeMessage(Long recipeId) {
this.recipeId = recipeId;
}
@Override
public Type getType() {
return Type.RECIPE_FAVOURITE;
}
/**
* gets the ID of the recipe that got favourite.
* @return
*/
public Long getRecipeId() {
return recipeId;
}
}

View file

@ -1,5 +1,21 @@
package commons.ws.messages; package commons.ws.messages;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = CreateRecipeMessage.class, name = "RECIPE_CREATE"),
@JsonSubTypes.Type(value = UpdateRecipeMessage.class, name = "RECIPE_UPDATE"),
@JsonSubTypes.Type(value = DeleteRecipeMessage.class, name = "RECIPE_DELETE"),
@JsonSubTypes.Type(value = CreateIngredientMessage.class, name = "INGREDIENT_CREATE"),
@JsonSubTypes.Type(value = UpdateIngredientMessage.class, name = "INGREDIENT_UPDATE"),
@JsonSubTypes.Type(value = DeleteIngredientMessage.class, name = "INGREDIENT_DELETE")
})
public interface Message { public interface Message {
public enum Type { public enum Type {
/** /**
@ -23,6 +39,15 @@ public interface Message {
*/ */
RECIPE_DELETE, RECIPE_DELETE,
/**
* Message sent when a recipe became a favourite.
*
* @see commons.ws.messages.FavouriteRecipeMessage
*/
RECIPE_FAVOURITE,
/** /**
* Message sent when a new ingredient is created. * Message sent when a new ingredient is created.
* *

View file

@ -10,6 +10,8 @@ import commons.Ingredient;
public class UpdateIngredientMessage implements Message { public class UpdateIngredientMessage implements Message {
private Ingredient ingredient; private Ingredient ingredient;
public UpdateIngredientMessage() {} // for jackson
public UpdateIngredientMessage(Ingredient ingredient) { public UpdateIngredientMessage(Ingredient ingredient) {
this.ingredient = ingredient; this.ingredient = ingredient;
} }
@ -27,4 +29,9 @@ public class UpdateIngredientMessage implements Message {
public Ingredient getIngredient() { public Ingredient getIngredient() {
return ingredient; return ingredient;
} }
// for jackson
public void setIngredient(Ingredient ingredient) {
this.ingredient = ingredient;
}
} }

View file

@ -10,6 +10,8 @@ import commons.Recipe;
public class UpdateRecipeMessage implements Message { public class UpdateRecipeMessage implements Message {
private Recipe recipe; private Recipe recipe;
public UpdateRecipeMessage() {} // for jackson
public UpdateRecipeMessage(Recipe recipe) { public UpdateRecipeMessage(Recipe recipe) {
this.recipe = recipe; this.recipe = recipe;
} }
@ -27,4 +29,9 @@ public class UpdateRecipeMessage implements Message {
public Recipe getRecipe() { public Recipe getRecipe() {
return recipe; return recipe;
} }
// for jackson
public void setRecipe(Recipe recipe) {
this.recipe = recipe;
}
} }

View file

@ -1,56 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package commons;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public class PersonTest {
@Test
public void checkConstructor() {
var p = new Person("f", "l");
assertEquals("f", p.firstName);
assertEquals("l", p.lastName);
}
@Test
public void equalsHashCode() {
var a = new Person("a", "b");
var b = new Person("a", "b");
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}
@Test
public void notEqualsHashCode() {
var a = new Person("a", "b");
var b = new Person("a", "c");
assertNotEquals(a, b);
assertNotEquals(a.hashCode(), b.hashCode());
}
@Test
public void hasToString() {
var actual = new Person("a", "b").toString();
assertTrue(actual.contains(Person.class.getSimpleName()));
assertTrue(actual.contains("\n"));
assertTrue(actual.contains("firstName"));
}
}

View file

@ -1,58 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package commons;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public class QuoteTest {
private static final Person SOME_PERSON = new Person("a", "b");
@Test
public void checkConstructor() {
var q = new Quote(SOME_PERSON, "q");
assertEquals(SOME_PERSON, q.person);
assertEquals("q", q.quote);
}
@Test
public void equalsHashCode() {
var a = new Quote(new Person("a", "b"), "c");
var b = new Quote(new Person("a", "b"), "c");
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}
@Test
public void notEqualsHashCode() {
var a = new Quote(new Person("a", "b"), "c");
var b = new Quote(new Person("a", "b"), "d");
assertNotEquals(a, b);
assertNotEquals(a.hashCode(), b.hashCode());
}
@Test
public void hasToString() {
var actual = new Quote(new Person("a", "b"), "c").toString();
assertTrue(actual.contains(Quote.class.getSimpleName()));
assertTrue(actual.contains("\n"));
assertTrue(actual.contains("person"));
}
}

View file

@ -1,33 +1,134 @@
# Week 3 meeting agenda # Week 6 meeting agenda
| Key | Value | | Key | Value |
| ------------ |-----------------------------------------------------------------------------------------| | ------------ |--------------------------------------------------------------------------------------------------|
| Date | Dec 18th | | Date | Dec 18th |
| Time | 14:45 | | Time | 14:45 |
| Location | Hall D | | Location | Hall D |
| Chair | Rithvik Sriram | | Chair | Rithvik Sriram |
| Minute Taker | Mei Chang van der Werff | | Minute Taker | Mei Chang van der Werff |
| Attendees | Mei Chang van der Werff, Natalia Cholewa, Rithvik Sriram, Aysegul Aydinlik, Steven Liu | | Attendees | Mei Chang van der Werff, Natalia Cholewa, Rithvik Sriram, Aysegul Aydinlik, Steven Liu (online) |
## Table of contents ## Table of contents
1. (1 min) Introduction by chair * (Steven joins the meeting online)
2. (1 min) Any additions to the agenda?
3. (1-2 min) TA announcements?
4. (1 min) Go over TA feedback given
5. (1 min) Next meeting thursday after midterms.
6. (33 min) **Meeting content**
1. (5 min) Week 6 progress in terms of completion
- How far are we in terms of completion?
2. (5 min) Progress check in terms of feature/issue completion
- What have we completed/going to complete this week?
3. (3 min) Small Showcase of the product
4. (20 min) **Sprint planning week 7-8** 1. (1 min) Introduction by chair
> Meeting starts at **14:46**
---
2. (1 min) Any additions to the agenda?
> Tomorrow is the deadline for technology, it's best to go over the requirements real quick. \
> To prevent failing this assignment like the previous 2
---
3. (1-2 min) TA announcements?
> If there are any questions about my feedback ask them now
4. (1 min) Go over TA feedback given
>*We've made a little summary of our feedback, and had a few questions*
> ### _Time Tracking_:
>- **What exactly is expected from us for the time tracking?** \
>We need to do an estimate of how long a task/issue might take. And after implementing it, you have to upload the actual taken time
>- **Are the Mr and issues time tracking connected?** \
>Yes, but not 100% sure. It's better to double-check it yourself
>
>### _Milestones_:
>- **What was the exact issue, they are too big?** \
>Our understanding of milestones is wrong, the milestones should be kinda our weekly tasks deadline. \
>And when we don't finish a task it should be moved to the next milestone, of the next week.
>
> ### _UserStory (backlog)_:
>- **How should we do this?** \
> You can just put it in the issue description.Don't make it your mr title! \
>A different groups makes the user story a parent issue and make children issues with the actual code, but we can choose what we want
>>For now, we will just put the user Story in the issue description, we will discuss it later if we want to change the method or not
>
>
>### _Commit and MR issues_:
>- Not all commits are related to the Mr
>- Our commits are too big, editing ±20 files are way too many. Try too keep it to 3-5 files max.
>- Also do only client or only server in a commit, don't combine them.
> - `mvn clean verify` before pushing to check whether the pipeline fails or not
---
5. (33 min) Meeting content
* (5 min) Week 6 progress in terms of completion
- How far are we in terms of completion?
>* 4.1 We still need to merge to connection of the print function, the Mr needed some changes but we can do that later. But do Rebase the MR as the branch is behind with A LOT of commits
>* 4.2 We've done the websockets, we only need to merge it
>* 4.3 We've done some backend parts but they are not yet connected to the client side
>* 4.4 The favourite button and message were made this week
>* 4.4 The search bar is currently in prgress on both the client and server side
>* 4.5 We keep this for week 8 or next week if there are not enough tasks
> >Let's focus on just connecting everything next week and after that we can start implementing 4.5
* (5 min) Progress check in terms of feature/issue completion
- What have we completed/going to complete this week?
> **Rithvik**: the backend of the searchbar and favourites is in progress. \
> **Aysegul**: Made the favourite button and some UI elements, sadly enough no penguin icon yet :< \
> **Oskar**: Finished the websocket implementation, however there is a small TODO left. Steven is going to do that. Also connected the config \
> **Mei Chang**: Deleted all the left over example code, made the favourite recipe message(backend), created a test class, made unit use enums and did a small TODO about setters.\
> **Natalia**: Almost finished the search bar (frontend) and improved testing on the client side. Will do some refactoring tomorrow. \
> **Steven**: Forgot to ask. But did make the language buttons a dropdown box, and did some other things
* (3 min) Small Showcase of the product
>We didn't have a lot of time to review and merge our code, so there is not big difference compared to last week. \
> We've shown the language dropdown box
* (20 min) Sprint planning week 7-8
- What needs to be done? - What needs to be done?
- Handle Backend-Server code delegation
- How do we do it? - How do we do it?
- Who does it? - Who does it?
5. (2 min) Designate teams. >We didn't really divide the tasks, just the roles. We will distribute tasks on the monday meeting. \
6. (1 min) Who is the next chair and minute taker? > We did discuss some things general tasks that need to be done
7. (2 min) Questions >
8. (1 min) Summarize everything > ### Server
9. (1 min) TA remarks > - Testing
**Max time:** ~40 minutes > - Refactoring (Oskar)
> - Some backend websocket things
> - Add more endpoints
>
> ### Client
> - Connecting everything to the UI
> - Some frontend websocket things
* (2 min) Designate teams.
>There is not a lot of server code that needs to be done so we're keeping the 2/4 server/client distribution this week.\
> Since there are still 3 people who need their LOC in server they have prioriy
> - Ayse and Oskar will do Server
> - The others will do Client
* (1 min) Who is the next chair and minute taker?
> Mei Chang will be the Chair \
> Aysegul will be the minute taker
---
>### Some Questions
> **What's the criteria of production code in server?**\
> Answer: recommendation to do 50/50 or 60/40 production/test code.
>
> **Can we copy code from the internet?**\
> Answer: Yes, but do put the link of where you got the code from in comments
>
> **For a test I want to change the gitlab UI, is that allowed?**
> **And I need to write a docx file for this, is the team fine with that?**\
> Answer TA: Yes, but make sure it doesn't affect the other teammates \
> Team: Yeah we're fine with that
---
6. (2 min) Questions
> #### We haven't yet talked about the technology requirements.
> We might didn't split the ui correctly, there is too much logic in it.\
> Server utils isn't server decorated
>
> **What is business logic?** \
> Something about the fact that not everything should be in the same file, but split across files
---
7. (1 min) Summarize everything
>- 3 people still need to do server LOC; Aysegul, Oskar, Mei Chang.
>- Next week our priority is connecting everything
>- We need to improve our code contribution and planning based on the feedback
---
8. (1 min) TA remarks \
Max time: ~40 minutes
> Ta will look at technology on Tuesday
>
>Meeting ends: **15:18**\
>Actual meeting time: 32 minutes

68
docs/feedback/week-06.md Normal file
View file

@ -0,0 +1,68 @@
# Meeting feedback
**Quick Summary Scale**
Insufficient/Sufficient/Good/Excellent
#### Agenda
Feedback: **Sufficient**
- The agenda was not uploaded on time. Generally, the agenda should be uploaded at least 2 days before the meeting so all members can access it, but I usually look for the agenda to be uploaded at the latest a day before the meeting.
- The agenda was formatted according to the template.
- The individual points were clear.
- Try to slightly adjust the layout of the agenda to be easier to read. For example, you can have sections like Opening, Updates, Feedback, Closing etc. It is easier to follow the agenda if the points are separated based on the discussed topics.
#### Performance of the Previous Minute Taker
Feedback: **Good**
- The notes have been merged in the agenda file.
- The notes are clear, but try to organize them a bit more as it is difficult to read through the notes when they are placed right next to each other.
- You can group the notes based on discussed points.
- The agreements are clear and realistic.
- Everyone was assigned to a task. For better visualization, you can add a task distribution table.
- A similar table can be used for minutes: one column for discussed topics and another column for notes.
#### Chair performance
Feedback: **Excellent**
- You did a good job with starting the meeting and checking in with everyone.
- You covered all points from the agenda.
- I liked that you took the initiative and guided the team through your points, e.g. when you discussed the feedback and contribution status.
- You asked for everyone's input, which is great. Well done!
- I also liked that you did a quick summary at the end.
#### Attitude & Relation
Feedback: **Good - Excellent**
- Overall the atmosphere was constructive and positive.
- All ideas were listened to and considered by all team members.
- Everyone was active and involved in the meeting.
- Everyone took ownership of the meeting.
- Most of you contributed to the discussion, but some seemed to be more quiet this week.
#### Potentially Shippable Product
Feedback: **Excellent**
- The team presented the current state of the application.
- The application is shippable, includes all basic requirements and great progress has been made on the extensions as well.
- Progress has been made compared to last week. Keep up the good work!
- You are on a good track to create a fully working application by the end, including all extensions! :)
#### Work Contribution/Distribution in the Team
Feedback: **Excellent**
- I liked that everyone explained what they did for this week.
- Most of you reached your goals. I understand that some items are still pending to be merged. Make sure you stay on track with the tasks of this week.
- So far it feels like everyone is still contributing equally to the team.
- Try to review what you planned the previous week and check if everything was completed.

View file

@ -1,52 +0,0 @@
/**
* Copyright 2024 Sebastian Proksch
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package server.api;
import java.util.LinkedList;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import commons.Person;
@RestController
@RequestMapping("/api/people")
public class PersonListingController {
private List<Person> people = new LinkedList<>();
public PersonListingController() {
people.add(new Person("Mickey", "Mouse"));
people.add(new Person("Donald", "Duck"));
}
@GetMapping("/")
public List<Person> list() {
return people;
}
@PostMapping("/")
public List<Person> add(@RequestBody Person p) {
if (!people.contains(p)) {
people.add(p);
}
return people;
}
}

View file

@ -1,79 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package server.api;
import java.util.List;
import java.util.Random;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import commons.Quote;
import server.database.QuoteRepository;
@RestController
@RequestMapping("/api/quotes")
public class QuoteController {
private final Random random;
private final QuoteRepository repo;
public QuoteController(Random random, QuoteRepository repo) {
this.random = random;
this.repo = repo;
}
@GetMapping(path = { "", "/" })
public List<Quote> getAll() {
return repo.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Quote> getById(@PathVariable("id") long id) {
if (id < 0 || !repo.existsById(id)) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(repo.findById(id).get());
}
@PostMapping(path = { "", "/" })
public ResponseEntity<Quote> add(@RequestBody Quote quote) {
if (quote.person == null || isNullOrEmpty(quote.person.firstName) || isNullOrEmpty(quote.person.lastName)
|| isNullOrEmpty(quote.quote)) {
return ResponseEntity.badRequest().build();
}
Quote saved = repo.save(quote);
return ResponseEntity.ok(saved);
}
private static boolean isNullOrEmpty(String s) {
return s == null || s.isEmpty();
}
@GetMapping("rnd")
public ResponseEntity<Quote> getRandom() {
var quotes = repo.findAll();
var idx = random.nextInt((int) repo.count());
return ResponseEntity.ok(quotes.get(idx));
}
}

View file

@ -1,22 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package server.database;
import org.springframework.data.jpa.repository.JpaRepository;
import commons.Quote;
public interface QuoteRepository extends JpaRepository<Quote, Long> {}

View file

@ -1,53 +0,0 @@
/**
* Copyright 2024 Sebastian Proksch
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package server.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import commons.Person;
public class PersonListingControllerTest {
private static final Person MICKEY = new Person("Mickey", "Mouse");
private static final Person DONALD = new Person("Donald", "Duck");
private static final Person SCROOGE = new Person("Scrooge", "McDuck");
private PersonListingController sut;
@BeforeEach
public void setup() {
sut = new PersonListingController();
}
@Test
public void containsTwoDefaultNames() {
var actual = sut.list();
var expected = List.of(MICKEY, DONALD);
assertEquals(expected, actual);
}
@Test
public void canAddPeople() {
var actual = sut.add(SCROOGE);
var expected = List.of(MICKEY, DONALD, SCROOGE);
assertEquals(expected, actual);
}
}

View file

@ -1,83 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package server.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import java.util.Random;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import commons.Person;
import commons.Quote;
public class QuoteControllerTest {
public int nextInt;
private MyRandom random;
private TestQuoteRepository repo;
private QuoteController sut;
@BeforeEach
public void setup() {
random = new MyRandom();
repo = new TestQuoteRepository();
sut = new QuoteController(random, repo);
}
@Test
public void cannotAddNullPerson() {
var actual = sut.add(getQuote(null));
assertEquals(BAD_REQUEST, actual.getStatusCode());
}
@Test
public void randomSelection() {
sut.add(getQuote("q1"));
sut.add(getQuote("q2"));
nextInt = 1;
var actual = sut.getRandom();
assertTrue(random.wasCalled);
assertEquals("q2", actual.getBody().quote);
}
@Test
public void databaseIsUsed() {
sut.add(getQuote("q1"));
repo.calledMethods.contains("save");
}
private static Quote getQuote(String q) {
return new Quote(new Person(q, q), q);
}
@SuppressWarnings("serial")
public class MyRandom extends Random {
public boolean wasCalled = false;
@Override
public int nextInt(int bound) {
wasCalled = true;
return nextInt;
}
}
}

View file

@ -1,225 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package server.api;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import commons.Quote;
import server.database.QuoteRepository;
public class TestQuoteRepository implements QuoteRepository {
public final List<Quote> quotes = new ArrayList<>();
public final List<String> calledMethods = new ArrayList<>();
private void call(String name) {
calledMethods.add(name);
}
@Override
public List<Quote> findAll() {
calledMethods.add("findAll");
return quotes;
}
@Override
public List<Quote> findAll(Sort sort) {
// TODO Auto-generated method stub
return null;
}
@Override
public List<Quote> findAllById(Iterable<Long> ids) {
// TODO Auto-generated method stub
return null;
}
@Override
public <S extends Quote> List<S> saveAll(Iterable<S> entities) {
// TODO Auto-generated method stub
return null;
}
@Override
public void flush() {
// TODO Auto-generated method stub
}
@Override
public <S extends Quote> S saveAndFlush(S entity) {
// TODO Auto-generated method stub
return null;
}
@Override
public <S extends Quote> List<S> saveAllAndFlush(Iterable<S> entities) {
// TODO Auto-generated method stub
return null;
}
@Override
public void deleteAllInBatch(Iterable<Quote> entities) {
// TODO Auto-generated method stub
}
@Override
public void deleteAllByIdInBatch(Iterable<Long> ids) {
// TODO Auto-generated method stub
}
@Override
public void deleteAllInBatch() {
// TODO Auto-generated method stub
}
@Override
public Quote getOne(Long id) {
// TODO Auto-generated method stub
return null;
}
@Override
public Quote getById(Long id) {
call("getById");
return find(id).get();
}
@Override
public Quote getReferenceById(Long id) {
call("getReferenceById");
return find(id).get();
}
private Optional<Quote> find(Long id) {
return quotes.stream().filter(q -> q.id == id).findFirst();
}
@Override
public <S extends Quote> List<S> findAll(Example<S> example) {
// TODO Auto-generated method stub
return null;
}
@Override
public <S extends Quote> List<S> findAll(Example<S> example, Sort sort) {
// TODO Auto-generated method stub
return null;
}
@Override
public Page<Quote> findAll(Pageable pageable) {
// TODO Auto-generated method stub
return null;
}
@Override
public <S extends Quote> S save(S entity) {
call("save");
entity.id = (long) quotes.size();
quotes.add(entity);
return entity;
}
@Override
public Optional<Quote> findById(Long id) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean existsById(Long id) {
call("existsById");
return find(id).isPresent();
}
@Override
public long count() {
return quotes.size();
}
@Override
public void deleteById(Long id) {
// TODO Auto-generated method stub
}
@Override
public void delete(Quote entity) {
// TODO Auto-generated method stub
}
@Override
public void deleteAllById(Iterable<? extends Long> ids) {
// TODO Auto-generated method stub
}
@Override
public void deleteAll(Iterable<? extends Quote> entities) {
// TODO Auto-generated method stub
}
@Override
public void deleteAll() {
// TODO Auto-generated method stub
}
@Override
public <S extends Quote> Optional<S> findOne(Example<S> example) {
// TODO Auto-generated method stub
return null;
}
@Override
public <S extends Quote> Page<S> findAll(Example<S> example, Pageable pageable) {
// TODO Auto-generated method stub
return null;
}
@Override
public <S extends Quote> long count(Example<S> example) {
// TODO Auto-generated method stub
return 0;
}
@Override
public <S extends Quote> boolean exists(Example<S> example) {
// TODO Auto-generated method stub
return false;
}
@Override
public <S extends Quote, R> R findBy(Example<S> example, Function<FetchableFluentQuery<S>, R> queryFunction) {
// TODO Auto-generated method stub
return null;
}
}