diff --git a/client/src/main/java/client/Main.java b/client/src/main/java/client/Main.java index f7c5a00..8be1175 100644 --- a/client/src/main/java/client/Main.java +++ b/client/src/main/java/client/Main.java @@ -25,7 +25,7 @@ import com.google.inject.Injector; import client.scenes.AddQuoteCtrl; import client.scenes.MainCtrl; import client.scenes.QuoteOverviewCtrl; -import client.utils.ServerUtils; +import client.utils.ServerUtilsExample; import javafx.application.Application; import javafx.stage.Stage; @@ -41,7 +41,7 @@ public class Main extends Application { @Override public void start(Stage primaryStage) throws Exception { - var serverUtils = INJECTOR.getInstance(ServerUtils.class); + var serverUtils = INJECTOR.getInstance(ServerUtilsExample.class); if (!serverUtils.isServerAvailable()) { var msg = "Server needs to be started before the client, but it does not seem to be available. Shutting down."; System.err.println(msg); diff --git a/client/src/main/java/client/scenes/AddQuoteCtrl.java b/client/src/main/java/client/scenes/AddQuoteCtrl.java index 0dcf39f..1ddf889 100644 --- a/client/src/main/java/client/scenes/AddQuoteCtrl.java +++ b/client/src/main/java/client/scenes/AddQuoteCtrl.java @@ -17,7 +17,7 @@ package client.scenes; import com.google.inject.Inject; -import client.utils.ServerUtils; +import client.utils.ServerUtilsExample; import commons.Person; import commons.Quote; import jakarta.ws.rs.WebApplicationException; @@ -29,7 +29,7 @@ import javafx.stage.Modality; public class AddQuoteCtrl { - private final ServerUtils server; + private final ServerUtilsExample server; private final MainCtrl mainCtrl; @FXML @@ -42,7 +42,7 @@ public class AddQuoteCtrl { private TextField quote; @Inject - public AddQuoteCtrl(ServerUtils server, MainCtrl mainCtrl) { + public AddQuoteCtrl(ServerUtilsExample server, MainCtrl mainCtrl) { this.mainCtrl = mainCtrl; this.server = server; diff --git a/client/src/main/java/client/scenes/QuoteOverviewCtrl.java b/client/src/main/java/client/scenes/QuoteOverviewCtrl.java index d97ebaf..11b64c6 100644 --- a/client/src/main/java/client/scenes/QuoteOverviewCtrl.java +++ b/client/src/main/java/client/scenes/QuoteOverviewCtrl.java @@ -20,7 +20,7 @@ import java.util.ResourceBundle; import com.google.inject.Inject; -import client.utils.ServerUtils; +import client.utils.ServerUtilsExample; import commons.Quote; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; @@ -32,7 +32,7 @@ import javafx.scene.control.TableView; public class QuoteOverviewCtrl implements Initializable { - private final ServerUtils server; + private final ServerUtilsExample server; private final MainCtrl mainCtrl; private ObservableList data; @@ -47,7 +47,7 @@ public class QuoteOverviewCtrl implements Initializable { private TableColumn colQuote; @Inject - public QuoteOverviewCtrl(ServerUtils server, MainCtrl mainCtrl) { + public QuoteOverviewCtrl(ServerUtilsExample server, MainCtrl mainCtrl) { this.server = server; this.mainCtrl = mainCtrl; } diff --git a/client/src/main/java/client/utils/ServerUtils.java b/client/src/main/java/client/utils/ServerUtils.java index 483eed1..37ebd30 100644 --- a/client/src/main/java/client/utils/ServerUtils.java +++ b/client/src/main/java/client/utils/ServerUtils.java @@ -1,64 +1,146 @@ -/* - * Copyright 2021 Delft University of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package client.utils; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import commons.Recipe; +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.List; + import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.ConnectException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; - -import org.glassfish.jersey.client.ClientConfig; - -import commons.Quote; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.GenericType; public class ServerUtils { + private static final String SERVER = "http://localhost:8080/api"; + private final HttpClient client; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final int statusOK = 200; - private static final String SERVER = "http://localhost:8080/"; + public ServerUtils() { + client = HttpClient.newHttpClient(); + } - public void getQuotesTheHardWay() throws IOException, URISyntaxException { - var url = new URI("http://localhost:8080/api/quotes").toURL(); - var is = url.openConnection().getInputStream(); - var br = new BufferedReader(new InputStreamReader(is)); - String line; - while ((line = br.readLine()) != null) { - System.out.println(line); + /** + * Gets all the recipes from the backend + * @return a JSON string with all the recipes + */ + public List getRecipes() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/recipes")) + .GET() + .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()); + } + + + return objectMapper.readValue(response.body(), new TypeReference>() { + });// JSON string-> List (Jackson) + } + + /** + * 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 = HttpRequest.newBuilder() + .uri(URI.create(SERVER +"/recipe/" + id)) + .GET() + .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(); + + + 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 = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/recipe/new")) + .header("Content-Type", "application/json") + .PUT(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 = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/recipe/" + id)) + .DELETE() + .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()); } } - public List getQuotes() { - return ClientBuilder.newClient(new ClientConfig()) // - .target(SERVER).path("api/quotes") // - .request(APPLICATION_JSON) // - .get(new GenericType>() {}); - } + /** + * 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 = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/recipe/" + id)) + .GET() + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - public Quote addQuote(Quote quote) { - return ClientBuilder.newClient(new ClientConfig()) // - .target(SERVER).path("api/quotes") // - .request(APPLICATION_JSON) // - .post(Entity.entity(quote, APPLICATION_JSON), Quote.class); + //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); + + recipe.setId(null); // otherwise the id is the same as the original, and that's wrong + + return addRecipe(recipe); } public boolean isServerAvailable() { diff --git a/client/src/main/java/client/utils/ServerUtilsExample.java b/client/src/main/java/client/utils/ServerUtilsExample.java new file mode 100644 index 0000000..bc4a187 --- /dev/null +++ b/client/src/main/java/client/utils/ServerUtilsExample.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Delft University of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package client.utils; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.net.ConnectException; +import java.util.List; + +import jakarta.ws.rs.core.GenericType; +import org.glassfish.jersey.client.ClientConfig; + +import commons.Quote; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; + +public class ServerUtilsExample { + + private static final String SERVER = "http://localhost:8080/"; + +// public void getQuotesTheHardWay() throws IOException, URISyntaxException { +// var url = new URI("http://localhost:8080/api/quotes").toURL(); +// var is = url.openConnection().getInputStream(); +// var br = new BufferedReader(new InputStreamReader(is)); +// String line; +// while ((line = br.readLine()) != null) { +// System.out.println(line); +// } +// } + + public List getQuotes() { + return ClientBuilder.newClient(new ClientConfig()) // + .target(SERVER).path("api/quotes") // + .request(APPLICATION_JSON) // + .get(new GenericType>() {}); + } + + public Quote addQuote(Quote quote) { + return ClientBuilder.newClient(new ClientConfig()) // + .target(SERVER).path("api/quotes") // + .request(APPLICATION_JSON) // + .post(Entity.entity(quote, APPLICATION_JSON), Quote.class); + } + + public boolean isServerAvailable() { + try { + ClientBuilder.newClient(new ClientConfig()) // + .target(SERVER) // + .request(APPLICATION_JSON) // + .get(); + } catch (ProcessingException e) { + if (e.getCause() instanceof ConnectException) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/client/src/test/java/client/ServerUtilsTest.java b/client/src/test/java/client/ServerUtilsTest.java new file mode 100644 index 0000000..47e4786 --- /dev/null +++ b/client/src/test/java/client/ServerUtilsTest.java @@ -0,0 +1,106 @@ +package client; + +import client.utils.ServerUtils; +import commons.Recipe; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ServerUtilsTest { + static ServerUtils dv; + static Recipe testRecipe; + final int fakeId = -1; // If suppose ID's are only positive + + @BeforeEach + void setup() throws IOException, InterruptedException { + dv = new ServerUtils(); + + Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available"); + + //Making sure there is no recipe in the backend yet + for (Recipe recipe : dv.getRecipes()) { + dv.deleteRecipe(recipe.getId()); + } + + Recipe r = new Recipe(); + + r.setName("Tosti"); + r.setIngredients(List.of("Bread", "Cheese", "Ham")); + r.setPreparationSteps(List.of("Step 1:", "Step 2")); + + testRecipe = dv.addRecipe(r); + } + + @Test + void addRecipeTest() throws IOException, InterruptedException { + Recipe r = new Recipe(); + r.setName("Eggs on toast"); + r.setIngredients(List.of("Bread", "egg", "salt")); + r.setPreparationSteps(List.of("Step 1:", "Step 2")); + + testRecipe = dv.addRecipe(r); + } + + @Test + void noRecipeToAdd(){ + Recipe r = new Recipe(); + assertThrows(IOException.class, () -> dv.addRecipe(r)); + + } + + @Test + void getAllRecipesTest() throws IOException, InterruptedException { + List recipes = dv.getRecipes(); + + assertNotNull(recipes, "The list should not be null"); + assertTrue(recipes.size() >= 0, "The list should be 0 (when no recipes), or more"); + } + + + @Test + void findRecipeWithIDTest() throws IOException, InterruptedException { + Recipe r = dv.findId(testRecipe.getId()); + + assertEquals(testRecipe.getId(), r.getId()); + assertEquals("Tosti", r.getName()); + assertIterableEquals(List.of("Bread", "Cheese", "Ham"), r.getIngredients()); + assertIterableEquals(List.of("Step 1:", "Step 2"), r.getPreparationSteps()); + } + + @Test + void noRecipeFoundTest() throws IOException, InterruptedException { + assertThrows(IOException.class, () -> dv.findId(fakeId)); + } + + @Test + void deleteRecipeTest() throws IOException, InterruptedException { + dv.deleteRecipe(testRecipe.getId()); + + assertThrows(IOException.class, () ->dv.findId(testRecipe.getId()), "The recipe shouldn't exists anymore" ); + } + + @Test + void noRecipeToDelete() throws IOException, InterruptedException { + assertThrows(IOException.class, () -> dv.deleteRecipe(fakeId)); + } + + @Test + void cloneRecipeTest() throws IOException, InterruptedException { + Recipe clone = dv.cloneRecipe(testRecipe.getId()); + + assertNotEquals(clone.getId(), testRecipe.getId(), "The id's should not be equal to each other"); + assertNotEquals(clone.getName(),testRecipe.getName(),"The name's should not be the same"); + assertEquals(clone.getIngredients(), testRecipe.getIngredients(), "the ingredients should be the same"); + assertEquals(clone.getPreparationSteps(), testRecipe.getPreparationSteps(),"The steps should be the same"); + } + + @Test + void noRecipeToClone(){ + assertThrows(IOException.class, () -> dv.cloneRecipe(fakeId)); + } +} \ No newline at end of file