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 getRecipes(List locales) throws IOException, InterruptedException { HttpRequest request = this.endpoints.fetchAllRecipes(locales).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != statusOK) { throw new IOException("No recipe to get. Server responds with " + response.body()); } List list = objectMapper.readValue(response.body(), new TypeReference>() {}); logger.info("Received response from server: " + list); return list; // JSON string-> List (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 getRecipesFiltered(String filter, List 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 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 list = objectMapper.readValue(response.body(), new TypeReference>() {}); 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 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 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 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 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 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 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 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 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 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 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 getIngredients() throws IOException, InterruptedException { HttpRequest request = this.endpoints.getIngredients().build(); HttpResponse 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>() {}); } //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 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 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); } }