From e41dc70f97bd77cccd5dc8cc2647027eb2e7275e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Wed, 17 Dec 2025 16:19:33 +0100 Subject: [PATCH 1/3] Modified Message and its subclasses to work nicely with jackson --- .../ws/messages/CreateIngredientMessage.java | 7 +++++++ .../commons/ws/messages/CreateRecipeMessage.java | 7 +++++++ .../ws/messages/DeleteIngredientMessage.java | 7 +++++++ .../commons/ws/messages/DeleteRecipeMessage.java | 7 +++++++ .../main/java/commons/ws/messages/Message.java | 16 ++++++++++++++++ .../ws/messages/UpdateIngredientMessage.java | 7 +++++++ .../commons/ws/messages/UpdateRecipeMessage.java | 7 +++++++ 7 files changed, 58 insertions(+) diff --git a/commons/src/main/java/commons/ws/messages/CreateIngredientMessage.java b/commons/src/main/java/commons/ws/messages/CreateIngredientMessage.java index a6392cb..8abd1b9 100644 --- a/commons/src/main/java/commons/ws/messages/CreateIngredientMessage.java +++ b/commons/src/main/java/commons/ws/messages/CreateIngredientMessage.java @@ -10,6 +10,8 @@ import commons.Ingredient; public class CreateIngredientMessage implements Message { private Ingredient ingredient; + public CreateIngredientMessage() {} // for jackson + public CreateIngredientMessage(Ingredient ingredient) { this.ingredient = ingredient; } @@ -27,4 +29,9 @@ public class CreateIngredientMessage implements Message { public Ingredient getIngredient() { return ingredient; } + + // for jackson + public void setIngredient(Ingredient ingredient) { + this.ingredient = ingredient; + } } diff --git a/commons/src/main/java/commons/ws/messages/CreateRecipeMessage.java b/commons/src/main/java/commons/ws/messages/CreateRecipeMessage.java index b036c1f..adc5b28 100644 --- a/commons/src/main/java/commons/ws/messages/CreateRecipeMessage.java +++ b/commons/src/main/java/commons/ws/messages/CreateRecipeMessage.java @@ -10,6 +10,8 @@ import commons.Recipe; public class CreateRecipeMessage implements Message { private Recipe recipe; + public CreateRecipeMessage() {} // for jackson + public CreateRecipeMessage(Recipe recipe) { this.recipe = recipe; } @@ -27,4 +29,9 @@ public class CreateRecipeMessage implements Message { public Recipe getRecipe() { return recipe; } + + // for jackson + public void setRecipe(Recipe recipe) { + this.recipe = recipe; + } } diff --git a/commons/src/main/java/commons/ws/messages/DeleteIngredientMessage.java b/commons/src/main/java/commons/ws/messages/DeleteIngredientMessage.java index 879df60..fc6985b 100644 --- a/commons/src/main/java/commons/ws/messages/DeleteIngredientMessage.java +++ b/commons/src/main/java/commons/ws/messages/DeleteIngredientMessage.java @@ -8,6 +8,8 @@ package commons.ws.messages; public class DeleteIngredientMessage implements Message { private Long ingredientId; + public DeleteIngredientMessage() {} // for jackson + public DeleteIngredientMessage(Long ingredientId) { this.ingredientId = ingredientId; } @@ -25,4 +27,9 @@ public class DeleteIngredientMessage implements Message { public Long getIngredientId() { return ingredientId; } + + // for jackson + public void setIngredientId(Long ingredientId) { + this.ingredientId = ingredientId; + } } diff --git a/commons/src/main/java/commons/ws/messages/DeleteRecipeMessage.java b/commons/src/main/java/commons/ws/messages/DeleteRecipeMessage.java index 1802525..8eab4e6 100644 --- a/commons/src/main/java/commons/ws/messages/DeleteRecipeMessage.java +++ b/commons/src/main/java/commons/ws/messages/DeleteRecipeMessage.java @@ -8,6 +8,8 @@ package commons.ws.messages; public class DeleteRecipeMessage implements Message { private Long recipeId; + public DeleteRecipeMessage() {} // for jackson + public DeleteRecipeMessage(Long recipeId) { this.recipeId = recipeId; } @@ -25,4 +27,9 @@ public class DeleteRecipeMessage implements Message { public Long getRecipeId() { return recipeId; } + + // for jackson + public void setRecipeId(Long recipeId) { + this.recipeId = recipeId; + } } diff --git a/commons/src/main/java/commons/ws/messages/Message.java b/commons/src/main/java/commons/ws/messages/Message.java index 9e20df0..5c05d7f 100644 --- a/commons/src/main/java/commons/ws/messages/Message.java +++ b/commons/src/main/java/commons/ws/messages/Message.java @@ -1,5 +1,21 @@ 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 enum Type { /** diff --git a/commons/src/main/java/commons/ws/messages/UpdateIngredientMessage.java b/commons/src/main/java/commons/ws/messages/UpdateIngredientMessage.java index e997873..e45e5a5 100644 --- a/commons/src/main/java/commons/ws/messages/UpdateIngredientMessage.java +++ b/commons/src/main/java/commons/ws/messages/UpdateIngredientMessage.java @@ -10,6 +10,8 @@ import commons.Ingredient; public class UpdateIngredientMessage implements Message { private Ingredient ingredient; + public UpdateIngredientMessage() {} // for jackson + public UpdateIngredientMessage(Ingredient ingredient) { this.ingredient = ingredient; } @@ -27,4 +29,9 @@ public class UpdateIngredientMessage implements Message { public Ingredient getIngredient() { return ingredient; } + + // for jackson + public void setIngredient(Ingredient ingredient) { + this.ingredient = ingredient; + } } diff --git a/commons/src/main/java/commons/ws/messages/UpdateRecipeMessage.java b/commons/src/main/java/commons/ws/messages/UpdateRecipeMessage.java index c623b75..72c9fd6 100644 --- a/commons/src/main/java/commons/ws/messages/UpdateRecipeMessage.java +++ b/commons/src/main/java/commons/ws/messages/UpdateRecipeMessage.java @@ -10,6 +10,8 @@ import commons.Recipe; public class UpdateRecipeMessage implements Message { private Recipe recipe; + public UpdateRecipeMessage() {} // for jackson + public UpdateRecipeMessage(Recipe recipe) { this.recipe = recipe; } @@ -27,4 +29,9 @@ public class UpdateRecipeMessage implements Message { public Recipe getRecipe() { return recipe; } + + // for jackson + public void setRecipe(Recipe recipe) { + this.recipe = recipe; + } } From 3da89c386b7595502b00ceeda17bca810c85383f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Wed, 17 Dec 2025 16:26:27 +0100 Subject: [PATCH 2/3] Created utils for websockets. --- client/pom.xml | 27 +++++ client/src/main/java/client/MyModule.java | 5 + .../java/client/utils/WebSocketUtils.java | 111 ++++++++++++++++++ commons/pom.xml | 6 + 4 files changed, 149 insertions(+) create mode 100644 client/src/main/java/client/utils/WebSocketUtils.java diff --git a/client/pom.xml b/client/pom.xml index d48d069..3bd793a 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -34,21 +34,25 @@ jersey-client ${version.jersey} + org.glassfish.jersey.inject jersey-hk2 ${version.jersey} + org.glassfish.jersey.media jersey-media-json-jackson ${version.jersey} + jakarta.activation jakarta.activation-api 2.1.3 + com.google.inject guice @@ -61,11 +65,13 @@ javafx-fxml ${version.jfx} + org.openjfx javafx-controls ${version.jfx} + org.openjfx javafx-web @@ -91,6 +97,27 @@ ${version.mockito} test + + + + org.springframework + spring-messaging + 6.2.12 + compile + + + + org.glassfish.tyrus.bundles + tyrus-standalone-client + 2.2.1 + + + org.springframework + spring-websocket + 6.2.12 + compile + + diff --git a/client/src/main/java/client/MyModule.java b/client/src/main/java/client/MyModule.java index 4f3464e..21da1dc 100644 --- a/client/src/main/java/client/MyModule.java +++ b/client/src/main/java/client/MyModule.java @@ -19,6 +19,8 @@ import client.scenes.FoodpalApplicationCtrl; import client.scenes.recipe.IngredientListCtrl; import client.scenes.recipe.RecipeStepListCtrl; import client.utils.LocaleManager; +import client.utils.ServerUtils; +import client.utils.WebSocketUtils; import com.google.inject.Binder; import com.google.inject.Module; import com.google.inject.Scopes; @@ -33,6 +35,9 @@ public class MyModule implements Module { binder.bind(FoodpalApplicationCtrl.class).in(Scopes.SINGLETON); binder.bind(IngredientListCtrl.class).in(Scopes.SINGLETON); binder.bind(RecipeStepListCtrl.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); } } \ No newline at end of file diff --git a/client/src/main/java/client/utils/WebSocketUtils.java b/client/src/main/java/client/utils/WebSocketUtils.java new file mode 100644 index 0000000..ed16b37 --- /dev/null +++ b/client/src/main/java/client/utils/WebSocketUtils.java @@ -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 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 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(); + } + +} diff --git a/commons/pom.xml b/commons/pom.xml index e5d5ff4..f48fbcf 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -45,6 +45,12 @@ ${version.mockito} test + + com.fasterxml.jackson.core + jackson-databind + 2.20.1 + compile + From 8bceffa67c0a28dbe4a45473a48262ce2aac9ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Wed, 17 Dec 2025 16:50:30 +0100 Subject: [PATCH 3/3] Implemented websockets in foodpal ctrl. --- .../client/scenes/FoodpalApplicationCtrl.java | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index 0b3aeba..2cb8d85 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -13,10 +13,14 @@ import client.utils.DefaultRecipeFactory; import client.utils.LocaleAware; import client.utils.LocaleManager; import client.utils.ServerUtils; +import client.utils.WebSocketUtils; import commons.Recipe; +import commons.ws.Topics; +import commons.ws.messages.Message; import jakarta.inject.Inject; +import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Alert; @@ -32,6 +36,7 @@ import javafx.scene.text.Font; public class FoodpalApplicationCtrl implements LocaleAware { private final ServerUtils server; + private final WebSocketUtils webSocketUtils; private final LocaleManager localeManager; private final IngredientListCtrl ingredientListCtrl; private final RecipeStepListCtrl stepListCtrl; @@ -81,15 +86,42 @@ public class FoodpalApplicationCtrl implements LocaleAware { @Inject public FoodpalApplicationCtrl( ServerUtils server, + WebSocketUtils webSocketUtils, LocaleManager localeManager, IngredientListCtrl ingredientListCtrl, RecipeStepListCtrl stepListCtrl ) { this.server = server; + this.webSocketUtils = webSocketUtils; this.localeManager = localeManager; this.ingredientListCtrl = ingredientListCtrl; this.stepListCtrl = stepListCtrl; + + initializeWebSocket(); } + + 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. + }); + }); + } + @Override public void initializeComponents() { // TODO Reduce code duplication?? @@ -146,6 +178,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { refresh(); } + private void showName(String name) { final int NAME_FONT_SIZE = 20; editableTitleArea.getChildren().clear(); @@ -153,6 +186,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { nameLabel.setFont(new Font("System Bold", NAME_FONT_SIZE)); editableTitleArea.getChildren().add(nameLabel); } + @Override public void updateText() { addRecipeButton.setText(getLocaleString("menu.button.add.recipe")); @@ -206,11 +240,13 @@ public class FoodpalApplicationCtrl implements LocaleAware { } detailsScreen.visibleProperty().set(!recipes.isEmpty()); } + private void printError(String msg) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setContentText(msg); alert.showAndWait(); } + /** * Adds a recipe, by providing a default name "Untitled recipe (n)" * The UX flow is now: @@ -283,7 +319,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { try { server.updateRecipe(selected); refresh(); - recipeList.getSelectionModel().select(selected); + //recipeList.getSelectionModel().select(selected); } catch (IOException | InterruptedException e) { // throw a nice blanket UpdateException throw new UpdateException("Error occurred when updating recipe name!"); @@ -294,7 +330,6 @@ public class FoodpalApplicationCtrl implements LocaleAware { edit.requestFocus(); } - // Language buttons @FXML private void switchLocale(ActionEvent event) {