Merge branch 'refactor/server-utils' into 'main'

Make ServerUtils use server address from config, refactor endpoint generation

See merge request cse1105/2025-2026/teams/csep-team-76!67
This commit is contained in:
Natalia Cholewa 2026-01-18 16:37:40 +01:00
commit 5a0503de10
15 changed files with 209 additions and 81 deletions

View file

@ -23,7 +23,7 @@ import client.scenes.recipe.IngredientListCtrl;
import client.scenes.recipe.RecipeStepListCtrl; 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.server.ServerUtils;
import client.utils.WebSocketDataService; import client.utils.WebSocketDataService;
import client.utils.WebSocketUtils; import client.utils.WebSocketUtils;
import com.google.inject.Binder; import com.google.inject.Binder;

View file

@ -2,7 +2,7 @@ package client;
import client.scenes.FoodpalApplicationCtrl; import client.scenes.FoodpalApplicationCtrl;
import client.scenes.MainCtrl; import client.scenes.MainCtrl;
import client.utils.ServerUtils; import client.utils.server.ServerUtils;
import com.google.inject.Injector; import com.google.inject.Injector;
import javafx.application.Application; import javafx.application.Application;
import javafx.scene.image.Image; import javafx.scene.image.Image;

View file

@ -18,7 +18,7 @@ import client.utils.ConfigService;
import client.utils.DefaultValueFactory; 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.server.ServerUtils;
import client.utils.WebSocketDataService; import client.utils.WebSocketDataService;
import client.utils.WebSocketUtils; import client.utils.WebSocketUtils;
import commons.Ingredient; import commons.Ingredient;

View file

@ -1,7 +1,7 @@
package client.scenes.Ingredient; package client.scenes.Ingredient;
import client.scenes.nutrition.NutritionDetailsCtrl; import client.scenes.nutrition.NutritionDetailsCtrl;
import client.utils.ServerUtils; import client.utils.server.ServerUtils;
import commons.Ingredient; import commons.Ingredient;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import javafx.fxml.FXML; import javafx.fxml.FXML;

View file

@ -3,7 +3,7 @@ package client.scenes;
import client.utils.ConfigService; 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.server.ServerUtils;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.Recipe; import commons.Recipe;
import javafx.animation.PauseTransition; import javafx.animation.PauseTransition;

View file

@ -4,7 +4,7 @@ import client.Ingredient.IngredientViewModel;
import client.exception.IllegalInputFormatException; import client.exception.IllegalInputFormatException;
import client.utils.LocaleAware; import client.utils.LocaleAware;
import client.utils.LocaleManager; import client.utils.LocaleManager;
import client.utils.ServerUtils; import client.utils.server.ServerUtils;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.Ingredient; import commons.Ingredient;
import javafx.application.Platform; import javafx.application.Platform;

View file

@ -1,6 +1,6 @@
package client.scenes.recipe; package client.scenes.recipe;
import client.utils.ServerUtils; import client.utils.server.ServerUtils;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.FormalIngredient; import commons.FormalIngredient;
import commons.Ingredient; import commons.Ingredient;

View file

@ -3,7 +3,7 @@ package client.scenes.recipe;
import client.utils.DefaultValueFactory; 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.server.ServerUtils;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.FormalIngredient; import commons.FormalIngredient;
import commons.Recipe; import commons.Recipe;

View file

@ -1,6 +1,6 @@
package client.scenes.recipe; package client.scenes.recipe;
import client.utils.ServerUtils; import client.utils.server.ServerUtils;
import commons.Ingredient; import commons.Ingredient;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import javafx.fxml.FXML; import javafx.fxml.FXML;

View file

@ -7,7 +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.PrintExportService; import client.utils.PrintExportService;
import client.utils.ServerUtils; import client.utils.server.ServerUtils;
import client.utils.WebSocketDataService; import client.utils.WebSocketDataService;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.Recipe; import commons.Recipe;

View file

@ -0,0 +1,94 @@
package client.utils.server;
import client.utils.ConfigService;
import com.google.inject.Inject;
import java.net.URI;
import java.net.http.HttpRequest;
import java.util.List;
public class Endpoints {
private final ConfigService configService;
@Inject
public Endpoints(ConfigService configService) {
this.configService = configService;
}
private HttpRequest.Builder http(String url) {
return HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json");
}
public String baseUrl() {
return this.configService.getConfig().getServerUrl() + "/api";
}
public String createApiUrl(String route) {
return this.baseUrl() + route;
}
public HttpRequest.Builder fetchAllRecipes(List<String> locales) {
String url = this.createApiUrl(
"/recipes" +
"?locales=" + String.join(",", locales)
);
return this.http(url).GET();
}
public HttpRequest.Builder fetchRecipe(long recipeId) {
String url = this.createApiUrl("/recipe/" + recipeId);
return this.http(url).GET();
}
public HttpRequest.Builder createNewRecipe(HttpRequest.BodyPublisher body) {
String url = this.createApiUrl("/recipe/new");
return this.http(url).PUT(body);
}
public HttpRequest.Builder updateRecipe(long recipeId, HttpRequest.BodyPublisher body) {
String url = this.createApiUrl("/recipe/" + recipeId);
return this.http(url).POST(body);
}
public HttpRequest.Builder deleteRecipe(long recipeId) {
String url = this.createApiUrl("/recipe/" + recipeId);
return this.http(url).DELETE();
}
public HttpRequest.Builder fetchIngredientUsage(long ingredientId) {
String url = this.createApiUrl("/ingredients/" + ingredientId + "/usage");
return this.http(url).GET();
}
public HttpRequest.Builder deleteIngredient(long ingredientId) {
String url = this.createApiUrl("/ingredients/" + ingredientId);
return this.http(url).DELETE();
}
public HttpRequest.Builder getIngredients() {
String url = this.createApiUrl("/ingredients");
return this.http(url).GET();
}
public HttpRequest.Builder createIngredient(HttpRequest.BodyPublisher body) {
String url = this.createApiUrl("/ingredients");
return this.http(url).POST(body);
}
public HttpRequest.Builder updateIngredient(long ingredientId, HttpRequest.BodyPublisher body) {
String url = this.createApiUrl("/ingredients/" + ingredientId);
return this.http(url).method("PATCH", body);
}
}

View file

@ -1,4 +1,4 @@
package client.utils; package client.utils.server;
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;
@ -13,7 +13,6 @@ import org.glassfish.jersey.client.ClientConfig;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
@ -25,15 +24,17 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
public class ServerUtils { public class ServerUtils {
private static final String SERVER = "http://localhost:8080/api";
private Logger logger = Logger.getLogger(ServerUtils.class.getName()); private Logger logger = Logger.getLogger(ServerUtils.class.getName());
private final HttpClient client; private final HttpClient client;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private final int statusOK = 200; private final int statusOK = 200;
private final Endpoints endpoints;
@Inject @Inject
public ServerUtils(HttpClient client) { public ServerUtils(HttpClient client, Endpoints endpoints) {
this.client = client; this.client = client;
this.endpoints = endpoints;
} }
/** /**
@ -41,25 +42,16 @@ public class ServerUtils {
* @return a JSON string with all the recipes * @return a JSON string with all the recipes
*/ */
public List<Recipe> getRecipes(List<String> locales) throws IOException, InterruptedException { public List<Recipe> getRecipes(List<String> locales) throws IOException, InterruptedException {
HttpRequest request = this.endpoints.fetchAllRecipes(locales).build();
String uri =
SERVER +
"/recipes" +
"?locales=" +
String.join(",", locales);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) { if (response.statusCode() != statusOK) {
throw new IOException("No recipe to get. Server responds with " + response.body()); throw new IOException("No recipe to get. Server responds with " + response.body());
} }
List<Recipe> list = objectMapper.readValue(response.body(), new TypeReference<List<Recipe>>() { List<Recipe> list = objectMapper.readValue(response.body(), new TypeReference<List<Recipe>>() {});
});
logger.info("Received response from server: " + list); logger.info("Received response from server: " + list);
return list; // JSON string-> List<Recipe> (Jackson) return list; // JSON string-> List<Recipe> (Jackson)
} }
@ -75,10 +67,8 @@ public class ServerUtils {
* @return a singe recipe with the given id * @return a singe recipe with the given id
*/ */
public Recipe findId(long id) throws IOException, InterruptedException { public Recipe findId(long id) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.fetchRecipe(id).build();
.uri(URI.create(SERVER +"/recipe/" + id))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){ if(response.statusCode() != statusOK){
@ -111,11 +101,9 @@ public class ServerUtils {
String json = objectMapper.writeValueAsString(newRecipe); String json = objectMapper.writeValueAsString(newRecipe);
//Recipe to backend //Recipe to backend
HttpRequest request = HttpRequest.newBuilder() HttpRequest request =
.uri(URI.create(SERVER + "/recipe/new")) this.endpoints.createNewRecipe(HttpRequest.BodyPublishers.ofString(json)).build();
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){ if(response.statusCode() != statusOK){
@ -129,10 +117,8 @@ public class ServerUtils {
* @param id the recipe that get deleted * @param id the recipe that get deleted
*/ */
public void deleteRecipe(long id) throws IOException, InterruptedException { public void deleteRecipe(long id) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.deleteRecipe(id).build();
.uri(URI.create(SERVER + "/recipe/" + id))
.DELETE()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){ if(response.statusCode() != statusOK){
@ -147,10 +133,8 @@ public class ServerUtils {
*/ */
public Recipe cloneRecipe(long id) throws IOException, InterruptedException { public Recipe cloneRecipe(long id) throws IOException, InterruptedException {
//Get the recipe //Get the recipe
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.fetchRecipe(id).build();
.uri(URI.create(SERVER + "/recipe/" + id))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
//200 is the status code for success, other codes can mean there is no recipe to clone //200 is the status code for success, other codes can mean there is no recipe to clone
@ -165,7 +149,7 @@ public class ServerUtils {
public boolean isServerAvailable() { public boolean isServerAvailable() {
try { try {
ClientBuilder.newClient(new ClientConfig()) // ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER) // .target(this.endpoints.baseUrl()) //
.request(APPLICATION_JSON) // .request(APPLICATION_JSON) //
.get(); .get();
} catch (ProcessingException e) { } catch (ProcessingException e) {
@ -194,11 +178,9 @@ public class ServerUtils {
public void updateRecipe(Recipe recipe) throws IOException, InterruptedException { public void updateRecipe(Recipe recipe) throws IOException, InterruptedException {
String json = objectMapper.writeValueAsString(recipe); String json = objectMapper.writeValueAsString(recipe);
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.updateRecipe(recipe.getId(), HttpRequest.BodyPublishers.ofString(json))
.uri(URI.create(SERVER + "/recipe/" + recipe.getId()))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString((json))) // Needs to be changed to PUT() when api changed
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){ if(response.statusCode() != statusOK){
@ -224,10 +206,7 @@ public class ServerUtils {
* @throws InterruptedException if operation is interrupted. * @throws InterruptedException if operation is interrupted.
*/ */
public long getIngredientUsage(long ingredientId) throws IOException, InterruptedException { public long getIngredientUsage(long ingredientId) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.fetchIngredientUsage(ingredientId).build();
.uri(URI.create(SERVER + "/ingredients/" + ingredientId + "/usage"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) { if (response.statusCode() != statusOK) {
@ -245,10 +224,7 @@ public class ServerUtils {
public void deleteIngredient(long ingredientId) throws IOException, InterruptedException { public void deleteIngredient(long ingredientId) throws IOException, InterruptedException {
// Send delete request to remove the ingredient // Send delete request to remove the ingredient
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.deleteIngredient(ingredientId).build();
.uri(URI.create(SERVER + "/ingredients/" + ingredientId))
.DELETE()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
@ -263,10 +239,7 @@ public class ServerUtils {
//retrieves the list of ingredients saved to backend //retrieves the list of ingredients saved to backend
public List<Ingredient> getIngredients() throws IOException, InterruptedException { public List<Ingredient> getIngredients() throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.getIngredients().build();
.uri(URI.create(SERVER + "/ingredients"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) { if (response.statusCode() != statusOK) {
@ -282,10 +255,7 @@ public class ServerUtils {
Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0); Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0);
String json = objectMapper.writeValueAsString(ingredient); String json = objectMapper.writeValueAsString(ingredient);
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.createIngredient(HttpRequest.BodyPublishers.ofString(json))
.uri(URI.create(SERVER + "/ingredients"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
@ -298,21 +268,15 @@ public class ServerUtils {
public Ingredient updateIngredient(Ingredient newIngredient) throws IOException, InterruptedException { public Ingredient updateIngredient(Ingredient newIngredient) throws IOException, InterruptedException {
logger.info("PATCH ingredient with id: " + newIngredient.getId()); logger.info("PATCH ingredient with id: " + newIngredient.getId());
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = this.endpoints.updateIngredient(
.uri(URI.create(SERVER + "/ingredients/" + newIngredient.getId())) newIngredient.getId(),
.header("Content-Type", "application/json") HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(newIngredient))
.method( ).build();
"PATCH",
HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(newIngredient)))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) { if (response.statusCode() != statusOK) {
throw new IOException("Failed to update ingredient with id: " + newIngredient.getId() + " body: " + response.body()); throw new IOException("Failed to update ingredient with id: " + newIngredient.getId() + " body: " + response.body());
} }
return objectMapper.readValue(response.body(), Ingredient.class); return objectMapper.readValue(response.body(), Ingredient.class);
} }
} }

View file

@ -0,0 +1,47 @@
package client;
import client.utils.ConfigService;
import client.utils.server.Endpoints;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class EndpointsTest {
static Endpoints endpoints;
@TempDir
Path tempDir;
@BeforeEach
void setup() throws IOException, InterruptedException {
var configPath = tempDir.resolve("configServiceTest.json");
var configService = new ConfigService(configPath);
configService.getConfig().setServerUrl("http://localhost:8080");
endpoints = new Endpoints(configService);
}
@Test
public void testEndpointForRecipe() {
var endpoint = endpoints.fetchRecipe(20);
var request = endpoint.build();
assertEquals("GET", request.method());
assertEquals("http://localhost:8080/api/recipe/20", request.uri().toString());
}
@Test
public void testEndpointForIngredientUsage() {
var endpoint = endpoints.fetchIngredientUsage(20);
var request = endpoint.build();
assertEquals("GET", request.method());
assertEquals("http://localhost:8080/api/ingredients/20/usage", request.uri().toString());
}
}

View file

@ -1,6 +1,8 @@
package client; package client;
import client.utils.ServerUtils; import client.utils.ConfigService;
import client.utils.server.Endpoints;
import client.utils.server.ServerUtils;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import commons.FormalIngredient; import commons.FormalIngredient;
@ -10,9 +12,11 @@ import commons.RecipeIngredient;
import commons.VagueIngredient; import commons.VagueIngredient;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException; import java.io.IOException;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -38,10 +42,18 @@ class ServerUtilsMockTest {
static final List<String> testPrepSteps = List.of("1. do smth", "2. do smth else"); static final List<String> testPrepSteps = List.of("1. do smth", "2. do smth else");
@TempDir
Path tempDir;
@BeforeEach @BeforeEach
void setup() { void setup() {
var configPath = tempDir.resolve("configServiceTest.json");
var configService = new ConfigService(configPath);
var endpoints = new Endpoints(configService);
objectMapper = new ObjectMapper(); objectMapper = new ObjectMapper();
serverUtils = new ServerUtils(HttpClient.newHttpClient()); serverUtils = new ServerUtils(HttpClient.newHttpClient(), endpoints);
} }
/** /**

View file

@ -1,6 +1,8 @@
package client; package client;
import client.utils.ServerUtils; import client.utils.ConfigService;
import client.utils.server.Endpoints;
import client.utils.server.ServerUtils;
import commons.Ingredient; import commons.Ingredient;
import commons.Recipe; import commons.Recipe;
import commons.RecipeIngredient; import commons.RecipeIngredient;
@ -9,9 +11,11 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException; import java.io.IOException;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -31,10 +35,17 @@ class ServerUtilsTest {
); );
final int fakeId = -1; // If suppose ID's are only positive final int fakeId = -1; // If suppose ID's are only positive
@TempDir
Path tempDir;
@BeforeEach @BeforeEach
void setup() throws IOException, InterruptedException { void setup() throws IOException, InterruptedException {
// FIXME: Prefer Guice-provided instance via a unit test Module. var configPath = tempDir.resolve("configServiceTest.json");
dv = new ServerUtils(HttpClient.newHttpClient()); var configService = new ConfigService(configPath);
configService.getConfig().setServerUrl("http://localhost:8080");
var endpoints = new Endpoints(configService);
dv = new ServerUtils(HttpClient.newHttpClient(), endpoints);
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available"); Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");