Merge branch 'client/incremental-websocket-modelling' into 'main'
fix(client/ws): Detailed WebSocket modelling Closes #31 See merge request cse1105/2025-2026/teams/csep-team-76!39
This commit is contained in:
commit
b01d313c9a
8 changed files with 203 additions and 47 deletions
|
|
@ -22,12 +22,16 @@ import client.scenes.recipe.RecipeStepListCtrl;
|
||||||
import client.utils.ConfigService;
|
import client.utils.ConfigService;
|
||||||
import client.utils.LocaleManager;
|
import client.utils.LocaleManager;
|
||||||
import client.utils.ServerUtils;
|
import client.utils.ServerUtils;
|
||||||
|
import client.utils.WebSocketDataService;
|
||||||
import client.utils.WebSocketUtils;
|
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 com.google.inject.TypeLiteral;
|
||||||
|
import commons.Ingredient;
|
||||||
|
import commons.Recipe;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
|
@ -45,6 +49,11 @@ public class MyModule implements Module {
|
||||||
binder.bind(WebSocketUtils.class).in(Scopes.SINGLETON);
|
binder.bind(WebSocketUtils.class).in(Scopes.SINGLETON);
|
||||||
|
|
||||||
binder.bind(ConfigService.class).toInstance(new ConfigService(Path.of("config.json")));
|
binder.bind(ConfigService.class).toInstance(new ConfigService(Path.of("config.json")));
|
||||||
|
binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance(
|
||||||
|
new WebSocketDataService<>()
|
||||||
|
);
|
||||||
|
binder.bind(new TypeLiteral<WebSocketDataService<Long, Ingredient>>() {}).toInstance(
|
||||||
|
new WebSocketDataService<>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package client.exception;
|
||||||
|
|
||||||
|
public class InvalidModificationException extends RuntimeException {
|
||||||
|
public InvalidModificationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,10 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import client.exception.InvalidModificationException;
|
||||||
import client.scenes.recipe.RecipeDetailCtrl;
|
import client.scenes.recipe.RecipeDetailCtrl;
|
||||||
|
|
||||||
import client.utils.Config;
|
import client.utils.Config;
|
||||||
|
|
@ -15,11 +17,16 @@ 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;
|
||||||
|
import client.utils.WebSocketDataService;
|
||||||
import client.utils.WebSocketUtils;
|
import client.utils.WebSocketUtils;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
|
|
||||||
import commons.ws.Topics;
|
import commons.ws.Topics;
|
||||||
|
import commons.ws.messages.CreateRecipeMessage;
|
||||||
|
import commons.ws.messages.DeleteRecipeMessage;
|
||||||
|
import commons.ws.messages.FavouriteRecipeMessage;
|
||||||
import commons.ws.messages.Message;
|
import commons.ws.messages.Message;
|
||||||
|
import commons.ws.messages.UpdateRecipeMessage;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
|
@ -29,11 +36,15 @@ 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;
|
||||||
import javafx.scene.control.ToggleButton;
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
public class FoodpalApplicationCtrl implements LocaleAware {
|
public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
private final ServerUtils server;
|
private final ServerUtils server;
|
||||||
private final WebSocketUtils webSocketUtils;
|
private final WebSocketUtils webSocketUtils;
|
||||||
private final LocaleManager localeManager;
|
private final LocaleManager localeManager;
|
||||||
|
private final WebSocketDataService<Long, Recipe> dataService;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private RecipeDetailCtrl recipeDetailController;
|
private RecipeDetailCtrl recipeDetailController;
|
||||||
|
|
@ -72,16 +83,72 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
ServerUtils server,
|
ServerUtils server,
|
||||||
WebSocketUtils webSocketUtils,
|
WebSocketUtils webSocketUtils,
|
||||||
LocaleManager localeManager,
|
LocaleManager localeManager,
|
||||||
ConfigService configService
|
ConfigService configService,
|
||||||
|
WebSocketDataService<Long, Recipe> recipeDataService
|
||||||
) {
|
) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.webSocketUtils = webSocketUtils;
|
this.webSocketUtils = webSocketUtils;
|
||||||
this.localeManager = localeManager;
|
this.localeManager = localeManager;
|
||||||
|
|
||||||
this.configService = configService;
|
this.configService = configService;
|
||||||
|
this.dataService = recipeDataService;
|
||||||
|
setupDataService();
|
||||||
initializeWebSocket();
|
initializeWebSocket();
|
||||||
}
|
}
|
||||||
|
private void setupDataService() {
|
||||||
|
dataService.setMessageParser((msg) -> switch (msg) {
|
||||||
|
case CreateRecipeMessage _ -> (m) -> {
|
||||||
|
CreateRecipeMessage crm = (CreateRecipeMessage) m;
|
||||||
|
return handleCreateRecipeMessage(crm);
|
||||||
|
};
|
||||||
|
case UpdateRecipeMessage _ -> (m) -> {
|
||||||
|
UpdateRecipeMessage urm = (UpdateRecipeMessage) m;
|
||||||
|
return handleUpdateRecipeMessage(urm);
|
||||||
|
};
|
||||||
|
case DeleteRecipeMessage _ -> (m) -> {
|
||||||
|
DeleteRecipeMessage drm = (DeleteRecipeMessage) m;
|
||||||
|
return handleDeleteRecipeMessage(drm);
|
||||||
|
};
|
||||||
|
case FavouriteRecipeMessage _ -> (m) -> {
|
||||||
|
FavouriteRecipeMessage frm = (FavouriteRecipeMessage) m;
|
||||||
|
return handleFavouriteRecipeMessage(frm);
|
||||||
|
};
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pair<Long, Recipe> handleCreateRecipeMessage(CreateRecipeMessage crm) {
|
||||||
|
this.recipeList.getItems().add(crm.getRecipe());
|
||||||
|
dataService.add(crm.getRecipe().getId());
|
||||||
|
return new ImmutablePair<>(crm.getRecipe().getId(), crm.getRecipe());
|
||||||
|
}
|
||||||
|
private Pair<Long, Recipe> handleUpdateRecipeMessage(UpdateRecipeMessage urm) {
|
||||||
|
Recipe recipe = urm.getRecipe();
|
||||||
|
// Discover the first (and should be ONLY) instance of the recipe with the specified ID.
|
||||||
|
// if not found, throw new InvalidModificationException with a message.
|
||||||
|
this.recipeList.getItems().set(
|
||||||
|
this.recipeList.getItems().indexOf(
|
||||||
|
findRecipeById(recipe.getId())
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new InvalidModificationException("Invalid recipe id during update: " + recipe.getId()))
|
||||||
|
),
|
||||||
|
recipe
|
||||||
|
);
|
||||||
|
dataService.add(recipe.getId());
|
||||||
|
return new ImmutablePair<>(recipe.getId(), recipe);
|
||||||
|
}
|
||||||
|
private Pair<Long, Recipe> handleDeleteRecipeMessage(DeleteRecipeMessage drm) {
|
||||||
|
this.recipeList.getItems().remove(findRecipeById(drm.getRecipeId()).orElseThrow(
|
||||||
|
() -> new InvalidModificationException("Invalid recipe id during delete: " + drm.getRecipeId())
|
||||||
|
));
|
||||||
|
dataService.add(drm.getRecipeId());
|
||||||
|
// TODO Make it an Optional<Recipe> so that we don't need to touch Null?
|
||||||
|
return new ImmutablePair<>(drm.getRecipeId(), null);
|
||||||
|
}
|
||||||
|
// TODO Implementation
|
||||||
|
private Pair<Long, Recipe> handleFavouriteRecipeMessage(FavouriteRecipeMessage frm) {
|
||||||
|
throw new NotImplementedException("TODO:: implement favourited recipe deletion warning");
|
||||||
|
}
|
||||||
|
|
||||||
private void initRecipeList() {
|
private void initRecipeList() {
|
||||||
// Show recipe name in the list
|
// Show recipe name in the list
|
||||||
|
|
@ -129,13 +196,17 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
private Optional<Recipe> findRecipeById(Long id) {
|
||||||
|
return this.recipeList.getItems().stream()
|
||||||
|
.filter(r -> r.getId().equals(id))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
private void initializeWebSocket() {
|
private void initializeWebSocket() {
|
||||||
webSocketUtils.connect(() -> {
|
webSocketUtils.connect(() -> {
|
||||||
webSocketUtils.subscribe(Topics.RECIPES, (Message _) -> {
|
webSocketUtils.subscribe(Topics.RECIPES, (Message msg) -> {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
dataService.onMessage(msg);
|
||||||
Recipe selectedRecipe = recipeList.getSelectionModel().getSelectedItem();
|
Recipe selectedRecipe = recipeList.getSelectionModel().getSelectedItem();
|
||||||
refresh(); // refresh the left list
|
|
||||||
if (selectedRecipe == null) {
|
if (selectedRecipe == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -220,15 +291,29 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
Recipe newRecipe = DefaultValueFactory.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
|
// Map of pending item updated, indexed by recipe id.
|
||||||
|
dataService.add(newRecipe.getId(), recipe -> {
|
||||||
// Select newly created recipe
|
this.recipeList.getSelectionModel().select(recipe);
|
||||||
recipeList.getSelectionModel().select(recipeList.getItems().indexOf(newRecipe));
|
openSelectedRecipe();
|
||||||
|
Platform.runLater(() -> this.recipeDetailController.editRecipeTitle());
|
||||||
|
});
|
||||||
|
recipeList.refresh();
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
printError("Error occurred when adding recipe!");
|
printError("Error occurred when adding recipe!");
|
||||||
}
|
}
|
||||||
// Calls edit after the recipe has been created, seamless name editing
|
}
|
||||||
// editRecipeTitle();
|
|
||||||
|
@FXML
|
||||||
|
public void removeSelectedRecipe() throws IOException, InterruptedException {
|
||||||
|
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
|
||||||
|
if (selected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.deleteRecipe(selected.getId());
|
||||||
|
dataService.add(selected.getId(), (r) -> {
|
||||||
|
// nothing to do here.
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
|
@ -241,24 +326,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
this.recipeDetailController.setVisible(true);
|
this.recipeDetailController.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the selected recipe from the server and refreshes the recipe list.
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
public void removeSelectedRecipe() throws IOException, InterruptedException {
|
|
||||||
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
|
|
||||||
if (selected == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
server.deleteRecipe(selected.getId());
|
|
||||||
|
|
||||||
// prefer a global refresh (or WS-based update upon its impl)
|
|
||||||
// rather than recipeList.getItems().remove(selected);
|
|
||||||
// to maintain single source of truth from the server.
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones a recipe, when clicking on the button "clone".
|
* Clones a recipe, when clicking on the button "clone".
|
||||||
*/
|
*/
|
||||||
|
|
@ -269,8 +336,10 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
}
|
}
|
||||||
|
|
||||||
Recipe cloned = server.cloneRecipe(selected.getId());
|
Recipe cloned = server.cloneRecipe(selected.getId());
|
||||||
refresh();
|
dataService.add(cloned.getId(), recipe -> {
|
||||||
recipeList.getSelectionModel().select(cloned);
|
recipeList.getSelectionModel().select(recipe);
|
||||||
|
openSelectedRecipe();}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,6 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateText() {
|
public void updateText() {
|
||||||
System.out.println("updatetext called on ingredientListCtrl");
|
|
||||||
ingredientsLabel.setText(getLocaleString("menu.label.ingredients"));
|
ingredientsLabel.setText(getLocaleString("menu.label.ingredients"));
|
||||||
addIngredientButton.setText(getLocaleString("menu.button.add.ingredient"));
|
addIngredientButton.setText(getLocaleString("menu.button.add.ingredient"));
|
||||||
deleteIngredientButton.setText(getLocaleString("menu.button.remove.ingredient"));
|
deleteIngredientButton.setText(getLocaleString("menu.button.remove.ingredient"));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import client.utils.ConfigService;
|
||||||
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.WebSocketDataService;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -22,6 +23,7 @@ import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for the recipe detail view.
|
* Controller for the recipe detail view.
|
||||||
|
|
@ -33,6 +35,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
private final ServerUtils server;
|
private final ServerUtils server;
|
||||||
private final FoodpalApplicationCtrl appCtrl;
|
private final FoodpalApplicationCtrl appCtrl;
|
||||||
private final ConfigService configService;
|
private final ConfigService configService;
|
||||||
|
private final WebSocketDataService<Long, Recipe> webSocketDataService;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private IngredientListCtrl ingredientListController;
|
private IngredientListCtrl ingredientListController;
|
||||||
|
|
@ -43,13 +46,17 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
private Recipe recipe;
|
private Recipe recipe;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RecipeDetailCtrl(LocaleManager localeManager, ServerUtils server,
|
public RecipeDetailCtrl(
|
||||||
|
LocaleManager localeManager,
|
||||||
|
ServerUtils server,
|
||||||
FoodpalApplicationCtrl appCtrl,
|
FoodpalApplicationCtrl appCtrl,
|
||||||
ConfigService configService) {
|
ConfigService configService,
|
||||||
|
WebSocketDataService<Long, Recipe> webSocketDataService) {
|
||||||
this.localeManager = localeManager;
|
this.localeManager = localeManager;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.appCtrl = appCtrl;
|
this.appCtrl = appCtrl;
|
||||||
this.configService = configService;
|
this.configService = configService;
|
||||||
|
this.webSocketDataService = webSocketDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
|
@ -154,6 +161,10 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
|
|
||||||
try { // propagate changes to server
|
try { // propagate changes to server
|
||||||
server.updateRecipe(selectedRecipe);
|
server.updateRecipe(selectedRecipe);
|
||||||
|
webSocketDataService.add(selectedRecipe.getId(), recipe -> {
|
||||||
|
int idx = getParentRecipeList().getItems().indexOf(selectedRecipe);
|
||||||
|
getParentRecipeList().getItems().set(idx, recipe);
|
||||||
|
});
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
throw new UpdateException("Unable to update recipe to server for " +
|
throw new UpdateException("Unable to update recipe to server for " +
|
||||||
selectedRecipe);
|
selectedRecipe);
|
||||||
|
|
@ -179,9 +190,10 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
* Revised edit recipe control flow, deprecates the use of a separate
|
* Revised edit recipe control flow, deprecates the use of a separate
|
||||||
* AddNameCtrl. This is automagically called when a new recipe is created,
|
* AddNameCtrl. This is automagically called when a new recipe is created,
|
||||||
* making for a more seamless UX.
|
* making for a more seamless UX.
|
||||||
|
* Public for reference by ApplicationCtrl.
|
||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void editRecipeTitle() {
|
public void editRecipeTitle() {
|
||||||
this.editableTitleArea.getChildren().clear();
|
this.editableTitleArea.getChildren().clear();
|
||||||
TextField edit = new TextField();
|
TextField edit = new TextField();
|
||||||
|
|
||||||
|
|
@ -217,15 +229,6 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
this.appCtrl.removeSelectedRecipe();
|
this.appCtrl.removeSelectedRecipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
|
||||||
/**
|
|
||||||
* Print the currently viewed recipe.
|
|
||||||
*/
|
|
||||||
private void printRecipe() {
|
|
||||||
// TODO: actually make it print?
|
|
||||||
System.out.println("Recipe printed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refreshes the favourite button state based on whether the currently viewed
|
* Refreshes the favourite button state based on whether the currently viewed
|
||||||
* recipe is marked as a favourite in the application configuration.
|
* recipe is marked as a favourite in the application configuration.
|
||||||
|
|
@ -242,6 +245,16 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
: "☆");
|
: "☆");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the currently viewed recipe.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void printRecipe() {
|
||||||
|
// TODO: actually make it print?
|
||||||
|
throw new NotImplementedException("TODO:: Integrate with Print/Export service");
|
||||||
|
// System.out.println("Recipe printed");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the favourite status of the currently viewed recipe in the
|
* Toggles the favourite status of the currently viewed recipe in the
|
||||||
* application configuration and writes the changes to disk.
|
* application configuration and writes the changes to disk.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,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 com.google.inject.Inject;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
import commons.RecipeIngredient;
|
import commons.RecipeIngredient;
|
||||||
import jakarta.ws.rs.ProcessingException;
|
import jakarta.ws.rs.ProcessingException;
|
||||||
|
|
@ -27,6 +28,7 @@ public class ServerUtils {
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private final int statusOK = 200;
|
private final int statusOK = 200;
|
||||||
|
|
||||||
|
@Inject
|
||||||
public ServerUtils() {
|
public ServerUtils() {
|
||||||
client = HttpClient.newHttpClient();
|
client = HttpClient.newHttpClient();
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +110,6 @@ public class ServerUtils {
|
||||||
if(response.statusCode() != statusOK){
|
if(response.statusCode() != statusOK){
|
||||||
throw new IOException("Failed to add Recipe: " + newRecipe.toDetailedString() + "body: " + response.body());
|
throw new IOException("Failed to add Recipe: " + newRecipe.toDetailedString() + "body: " + response.body());
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(response.body(),Recipe.class);
|
return objectMapper.readValue(response.body(),Recipe.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
58
client/src/main/java/client/utils/WebSocketDataService.java
Normal file
58
client/src/main/java/client/utils/WebSocketDataService.java
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package client.utils;
|
||||||
|
|
||||||
|
import client.exception.InvalidModificationException;
|
||||||
|
import commons.ws.messages.Message;
|
||||||
|
import org.apache.commons.lang3.function.FailableFunction;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class WebSocketDataService<ID, Value> {
|
||||||
|
public WebSocketDataService() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dataRegister maps a CompletableFuture<T> to its fulfilment function.
|
||||||
|
*/
|
||||||
|
private final Map<ID, CompletableFuture<Value>> pendingRegister = new HashMap<>();
|
||||||
|
private Function<Message, FailableFunction<Message, Pair<ID, Value>, InvalidModificationException>> parser;
|
||||||
|
public void setMessageParser(
|
||||||
|
Function<
|
||||||
|
Message,
|
||||||
|
FailableFunction<Message, Pair<ID, Value>, InvalidModificationException>
|
||||||
|
> parser) {
|
||||||
|
this.parser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On each WS message, the parser callback distinguishes what
|
||||||
|
* consumer (crud function) to call. Then the pair of ID-Value
|
||||||
|
* returned by the processor is used
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
|
public void onMessage(Message message) {
|
||||||
|
Pair<ID, Value> result = parser.apply(message).apply(message);
|
||||||
|
pendingRegister.get(result.getKey()).complete(result.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an ID-reference to the map for an object whose update is pending.
|
||||||
|
* If the key already exists, the caller should assume there is an ongoing
|
||||||
|
* operation on the object.
|
||||||
|
* @param id ID of the object.
|
||||||
|
* @return Whether the key already exists.
|
||||||
|
*/
|
||||||
|
public boolean add(ID id, Consumer<Value> onComplete) {
|
||||||
|
CompletableFuture<Value> future = new CompletableFuture<>();
|
||||||
|
future.thenAccept(onComplete.andThen(_ -> pendingRegister.remove(id)));
|
||||||
|
return pendingRegister.putIfAbsent(id, future) == null;
|
||||||
|
}
|
||||||
|
public boolean add(ID id) {
|
||||||
|
return add(id, (_) -> {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ public class PrintExportTest {
|
||||||
assertEquals("""
|
assertEquals("""
|
||||||
Title: Banana Bread
|
Title: Banana Bread
|
||||||
Recipe ID: 1234
|
Recipe ID: 1234
|
||||||
Ingredients: some Banana, some Bread,\s
|
Ingredients: Some Banana, Some Bread,\s
|
||||||
Steps:
|
Steps:
|
||||||
1: Mix Ingredients
|
1: Mix Ingredients
|
||||||
2: Heat in Oven
|
2: Heat in Oven
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue