csep-2025/client/src/main/java/client/utils/server/ServerUtils.java

305 lines
12 KiB
Java

package client.utils.server;
import client.utils.ConfigService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
import commons.Ingredient;
import commons.Recipe;
import commons.RecipeIngredient;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ClientBuilder;
import org.glassfish.jersey.client.ClientConfig;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
public class ServerUtils {
private Logger logger = Logger.getLogger(ServerUtils.class.getName());
private final HttpClient client;
private final ObjectMapper objectMapper = new ObjectMapper();
private final ConfigService configService;
private final int statusOK = 200;
private final Endpoints endpoints;
@Inject
public ServerUtils(HttpClient client, Endpoints endpoints, ConfigService configService) {
this.client = client;
this.endpoints = endpoints;
this.configService = configService;
}
/**
* Gets all the recipes from the backend.
* @return a JSON string with all the recipes
*/
public List<Recipe> getRecipes(List<String> locales) throws IOException, InterruptedException {
HttpRequest request = this.endpoints.fetchAllRecipes(locales).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("No recipe to get. Server responds with " + response.body());
}
List<Recipe> list = objectMapper.readValue(response.body(), new TypeReference<List<Recipe>>() {});
logger.info("Received response from server: " + list);
return list; // JSON string-> List<Recipe> (Jackson)
}
/**
* The method used by the search bar to get filtered recipes.
* @param filter - filter string
* @param locales - locales of the user
* @return filtered recipe list
*/
public List<Recipe> getRecipesFiltered(String filter, List<String> locales) throws IOException, InterruptedException {
//TODO add limit integration
String uri = configService.getConfig().getServerUrl() + "/recipes?search=" + filter + "&locales=" + String.join(",", locales);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed to get filtered recipes. Server responds with " + response.body());
}
List<Recipe> list = objectMapper.readValue(response.body(), new TypeReference<List<Recipe>>() {});
logger.info("Received filtered recipes from server: " + list);
return list;
}
/**
* Gets a single recipe based on its id.
* @param id every recipe has it's unique id
* @return a singe recipe with the given id
*/
public Recipe findId(long id) throws IOException, InterruptedException {
HttpRequest request = this.endpoints.fetchRecipe(id).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("failed finding recipe with id: "+ id + " body: " + response.body());
}
return objectMapper.readValue(response.body(),Recipe.class);
}
/**
* Adds a recipe to the backend.
* @param newRecipe the recipe to be added
* @return a recipe
*/
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
//Make sure the name of the newRecipe is unique
List<Recipe> allRecipes = getRecipes(List.of());
newRecipe.setId(null); // otherwise the id is the same as the original, and that's wrong
// now that each recipeIngredient has its own ID in the database,
// we set that to null too to force a new persist value on the server
newRecipe.getIngredients().forEach(ingredient -> ingredient.setId(null));
int version = 1;
String originalName = newRecipe.getName();
while (allRecipes.stream().anyMatch(r -> r.getName().equals(newRecipe.getName()))) {
String newName = originalName + "(" + version++ + ")";
newRecipe.setName(newName);
}
String json = objectMapper.writeValueAsString(newRecipe);
//Recipe to backend
HttpRequest request =
this.endpoints.createNewRecipe(HttpRequest.BodyPublishers.ofString(json)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed to add Recipe: " + newRecipe.toDetailedString() + "body: " + response.body());
}
return objectMapper.readValue(response.body(),Recipe.class);
}
/**
* Deletes a recipe.
* @param id the recipe that get deleted
*/
public void deleteRecipe(long id) throws IOException, InterruptedException {
HttpRequest request = this.endpoints.deleteRecipe(id).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed removing recipe with id: " + id + "body: " + response.body());
}
}
/**
* Clones a recipe.
* @param id the id of the recipe to be cloned
* @return a duplicated recipe of the given recipe
*/
public Recipe cloneRecipe(long id) throws IOException, InterruptedException {
//Get the recipe
HttpRequest request = this.endpoints.fetchRecipe(id).build();
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
if(response.statusCode() != statusOK){
throw new IOException("Failed to get recipe to clone with id: " + id + "body: " + response.body());
}
// recipe exists so you can make a "new" recipe aka the clone
Recipe recipe = objectMapper.readValue(response.body(), Recipe.class);
return addRecipe(recipe);
}
public boolean isServerAvailable() {
try {
ClientBuilder.newClient(new ClientConfig()) //
.target(this.endpoints.baseUrl()) //
.request(APPLICATION_JSON) //
.get();
} catch (ProcessingException e) {
if (e.getCause() instanceof ConnectException) {
return false;
}
}
return true;
}
public void addRecipeName(String name) throws IOException, InterruptedException {
Recipe newRecipe = new Recipe();
newRecipe.setName(name);
addRecipe(newRecipe);
}
public void addRecipeIngredient(Recipe recipe, RecipeIngredient ingredient) throws IOException, InterruptedException {
List<RecipeIngredient> ingredients = new ArrayList<>(recipe.getIngredients());
ingredients.add(ingredient);
recipe.setIngredients(ingredients);
updateRecipe(recipe);
}
public void updateRecipe(Recipe recipe) throws IOException, InterruptedException {
String json = objectMapper.writeValueAsString(recipe);
HttpRequest request = this.endpoints.updateRecipe(recipe.getId(), HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed to update recipe: " + recipe.toDetailedString() + "body: " + response.body());
}
objectMapper.readValue(response.body(), Recipe.class);
}
public void addRecipeStep(Recipe recipe, String preparationStep) throws IOException, InterruptedException {
List<String> preparationSteps = new ArrayList<>(recipe.getPreparationSteps());
preparationSteps.add(preparationStep);
recipe.setPreparationSteps(preparationSteps);
updateRecipe(recipe);
}
/**
* Gets the amount of recipes this ingredient is being used in.
* @param ingredientId The queried ingredient's ID.
* @return The amount of recipes the ingredient is used in.
* @throws IOException if server query failed.
* @throws InterruptedException if operation is interrupted.
*/
public long getIngredientUsage(long ingredientId) throws IOException, InterruptedException {
HttpRequest request = this.endpoints.fetchIngredientUsage(ingredientId).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to get usage for ingredient with id: " + ingredientId
+ " body: " + response.body());
}
record IngredientUsageResponse(long ingredientId, long usedInRecipes) {}
IngredientUsageResponse usage =
objectMapper.readValue(response.body(), IngredientUsageResponse.class);
return usage.usedInRecipes();
}
public void deleteIngredient(long ingredientId) throws IOException, InterruptedException {
// Send delete request to remove the ingredient
HttpRequest request = this.endpoints.deleteIngredient(ingredientId).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to delete ingredient with id: " + ingredientId + " body: " + response.body());
}
logger.info("Successfully deleted ingredient with id: " + ingredientId);
}
//retrieves the list of ingredients saved to backend
public List<Ingredient> getIngredients() throws IOException, InterruptedException {
HttpRequest request = this.endpoints.getIngredients().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to fetch ingredients. Server responds with: " + response.body());
}
return objectMapper.readValue(response.body(), new com.fasterxml.jackson.core.type.TypeReference<List<Ingredient>>() {});
}
//creates new ingredients in the ingredient list
public Ingredient createIngredient(String name) throws IOException, InterruptedException {
Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0);
String json = objectMapper.writeValueAsString(ingredient);
HttpRequest request = this.endpoints.createIngredient(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to create ingredient. Server responds with: " + response.body());
}
return objectMapper.readValue(response.body(), Ingredient.class);
}
public Ingredient updateIngredient(Ingredient newIngredient) throws IOException, InterruptedException {
logger.info("PATCH ingredient with id: " + newIngredient.getId());
HttpRequest request = this.endpoints.updateIngredient(
newIngredient.getId(),
HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(newIngredient))
).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to update ingredient with id: " + newIngredient.getId() + " body: " + response.body());
}
return objectMapper.readValue(response.body(), Ingredient.class);
}
}