From 23a015807e661d66e9cea6a97872afc679736323 Mon Sep 17 00:00:00 2001 From: Allen Keng Date: Mon, 4 Dec 2023 12:28:36 -0800 Subject: [PATCH 01/31] fix: meal type parsing and account creation Co-authored-by: Samantha Prestrelski --- .gitignore | 3 +- .../code/client/Controllers/Controller.java | 122 ++++++++++-------- .../code/client/Model/MockWhisperService.java | 2 +- .../main/java/code/client/Model/Model.java | 6 +- .../code/client/View/AccountCreationUI.java | 15 +++ .../java/code/client/View/AppFrameMic.java | 15 ++- .../code/server/AccountRequestHandler.java | 2 +- 7 files changed, 97 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index 408eaf0..0041cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ gradle-app.setting *.properties .vscode/launch.json !gradle/wrapper/gradle-wrapper.jar -!gradle/wrapper/gradle-wrapper.properties \ No newline at end of file +!gradle/wrapper/gradle-wrapper.properties +*.csv \ No newline at end of file diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index a4e2324..a3d4920 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.UUID; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + import code.client.View.RecipeListUI; import code.client.View.RecipeUI; import code.client.View.View; @@ -24,6 +26,8 @@ import javafx.scene.control.Hyperlink; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import javafx.scene.layout.GridPane; import javafx.scene.paint.Color; import javafx.scene.text.Font; @@ -37,15 +41,19 @@ import code.server.Recipe; public class Controller { - // Index of newest to oldest in sorting drop down menu, index of breakfast in filtering drop down menu + // Index of newest to oldest in sorting drop down menu, index of breakfast in + // filtering drop down menu private final int NEWEST_TO_OLDEST_INDEX = 0, BREAKFAST_INDEX = 0; - // Index of oldest to newest in sorting drop down menu, index of lunch in filtering drop down menu + // Index of oldest to newest in sorting drop down menu, index of lunch in + // filtering drop down menu private final int OLDEST_TO_NEWEST_INDEX = 1, LUNCH_INDEX = 1; - // Index of A to Z in sorting drop down menu, index of dinner in filtering drop down menu + // Index of A to Z in sorting drop down menu, index of dinner in filtering drop + // down menu private final int A_TO_Z_INDEX = 2, DINNER_INDEX = 2; - // Index of Z to A in sorting drop down menu, index of none in filtering drop down menu + // Index of Z to A in sorting drop down menu, index of none in filtering drop + // down menu private final int Z_TO_A_INDEX = 3, NONE_INDEX = 3; - + private Account account; private Model model; private View view; @@ -74,27 +82,12 @@ public Controller(View view, Model model) { } }); - // this.view.getAppFrameHome().setSortMenuButtonAction(event -> { - // try { - // handleSortMenuButton(event); - // } catch (Exception e) { - // e.printStackTrace(); - // } - // }); - - // this.view.getAppFrameHome().setFilterMenuButtonAction(event -> { - // try { - // handleFilterMenuButton(event); - // } catch (Exception e) { - // e.printStackTrace(); - // } - // }); - this.view.getAppFrameHome().setLogOutButtonAction(event -> { handleLogOutOutButton(event); }); this.view.getAccountCreationUI().setCreateAccountButtonAction(this::handleCreateAcc); + this.view.getAccountCreationUI().setGoToLoginAction(this::handleGoToLogin); this.view.getLoginUI().setGoToCreateAction(this::handleGoToCreateLogin); this.view.getLoginUI().setLoginButtonAction(this::handleLoginButton); loadCredentials(); @@ -169,11 +162,11 @@ private void handleNewButton(ActionEvent event) throws URISyntaxException, IOExc } // private void handleSortButton(ActionEvent event) { - // ChoiceBox sortChoiceBox = view.getAppFrameHome().getSortChoiceBox(); - // RecipeListUI list = view.getAppFrameHome().getRecipeList(); - // view.getAppFrameHome().getSortButton().setOnAction(e -> { - // sortChoiceBox.show(); - // }); + // ChoiceBox sortChoiceBox = view.getAppFrameHome().getSortChoiceBox(); + // RecipeListUI list = view.getAppFrameHome().getRecipeList(); + // view.getAppFrameHome().getSortButton().setOnAction(e -> { + // sortChoiceBox.show(); + // }); // } private void addFilterListeners() { @@ -183,28 +176,28 @@ private void addFilterListeners() { // Filter to show only breakfast recipes when criteria is selected filterMenuItems.get(BREAKFAST_INDEX).setOnAction(e -> { filter = "breakfast"; - setActiveState(filterMenuButton,BREAKFAST_INDEX); + setActiveState(filterMenuButton, BREAKFAST_INDEX); this.view.getAppFrameHome().updateDisplay("breakfast"); addListenersToList(); }); // Filter to show only lunch recipes when criteria is selected filterMenuItems.get(LUNCH_INDEX).setOnAction(e -> { filter = "lunch"; - setActiveState(filterMenuButton,LUNCH_INDEX); + setActiveState(filterMenuButton, LUNCH_INDEX); this.view.getAppFrameHome().updateDisplay("lunch"); addListenersToList(); }); // Filter to show only dinner recipes when criteria is selected filterMenuItems.get(DINNER_INDEX).setOnAction(e -> { filter = "dinner"; - setActiveState(filterMenuButton,DINNER_INDEX); + setActiveState(filterMenuButton, DINNER_INDEX); this.view.getAppFrameHome().updateDisplay("dinner"); addListenersToList(); }); // Remove selected filter to show all recipes filterMenuItems.get(NONE_INDEX).setOnAction(e -> { filter = "none"; - setActiveState(filterMenuButton,NONE_INDEX); + setActiveState(filterMenuButton, NONE_INDEX); this.view.getAppFrameHome().updateDisplay("none"); addListenersToList(); }); @@ -214,49 +207,49 @@ private void addSortingListener() { RecipeListUI list = this.view.getAppFrameHome().getRecipeList(); MenuButton sortMenuButton = view.getAppFrameHome().getSortMenuButton(); ObservableList sortMenuItems = sortMenuButton.getItems(); + if (list.getRecipeDB() == null) { + return; + } RecipeSorter recipeSorter = new RecipeSorter(list.getRecipeDB().getList()); - + // Setting action for newest to oldest sorting criteria sortMenuItems.get(NEWEST_TO_OLDEST_INDEX).setOnAction(e -> { recipeSorter.sortNewestToOldest(); - setActiveState(sortMenuButton, NEWEST_TO_OLDEST_INDEX); - this.view.getAppFrameHome().updateDisplay(filter); - addListenersToList(); + sortList(sortMenuButton, NEWEST_TO_OLDEST_INDEX); }); // Setting action for oldest to newest sorting criteria sortMenuItems.get(OLDEST_TO_NEWEST_INDEX).setOnAction(e -> { recipeSorter.sortOldestToNewest(); - setActiveState(sortMenuButton, OLDEST_TO_NEWEST_INDEX); - this.view.getAppFrameHome().updateDisplay(filter); - addListenersToList(); + sortList(sortMenuButton, OLDEST_TO_NEWEST_INDEX); }); // Setting action for A to Z sorting criteria sortMenuItems.get(A_TO_Z_INDEX).setOnAction(e -> { recipeSorter.sortAToZ(); - setActiveState(sortMenuButton, A_TO_Z_INDEX); - this.view.getAppFrameHome().updateDisplay(filter); - addListenersToList(); + sortList(sortMenuButton, A_TO_Z_INDEX); }); // Setting action for Z to A sorting criteria sortMenuItems.get(Z_TO_A_INDEX).setOnAction(e -> { recipeSorter.sortZToA(); - setActiveState(sortMenuButton, Z_TO_A_INDEX); - this.view.getAppFrameHome().updateDisplay(filter); - addListenersToList(); + sortList(sortMenuButton, Z_TO_A_INDEX); }); } + private void sortList(MenuButton sortMenuButton, int index) { + setActiveState(sortMenuButton, index); + this.view.getAppFrameHome().updateDisplay(filter); + addListenersToList(); + } + private void setActiveState(MenuButton items, int index) { - for(int i =0; i < NONE_INDEX + 1; i++) { - if(i == index) { + for (int i = 0; i < NONE_INDEX + 1; i++) { + if (i == index) { items.getItems().get(i).setStyle("-fx-background-color: #90EE90"); - } - else { + } else { items.getItems().get(i).setStyle("-fx-background-color: transparent;"); } } } - + private void handleLogOutOutButton(ActionEvent event) { clearCredentials(); view.goToLoginUI(); @@ -278,7 +271,8 @@ public void addListenersToList() { try { deleteGivenRecipe(currRecipe.getRecipe()); } catch (IOException e1) { - System.out.println("Could not delete recipe with id:title of " + currRecipe.getRecipe().getId() + ":" + currRecipe.getRecipe().getTitle() ); + System.out.println("Could not delete recipe with id:title of " + currRecipe.getRecipe().getId() + + ":" + currRecipe.getRecipe().getTitle()); e1.printStackTrace(); } }); @@ -359,8 +353,9 @@ private void handleDeleteButton(ActionEvent event) throws IOException { DetailsAppFrame detailsAppFrame = this.view.getDetailedView(); Recipe displayed = detailsAppFrame.getDisplayedRecipe(); deleteGivenRecipe(displayed); - + } + private void deleteGivenRecipe(Recipe recipe) throws IOException { Writer writer = new StringWriter(); recipeWriter = new RecipeCSVWriter(writer); @@ -376,8 +371,6 @@ private void deleteGivenRecipe(Recipe recipe) throws IOException { private void handleShareButton(ActionEvent event) { Recipe shownRecipe = this.view.getDetailedView().getDisplayedRecipe(); String id = shownRecipe.getId(); - - String styleAlert = "-fx-background-color: #F1FFCB; -fx-font-weight: bold;"; Hyperlink textArea = new Hyperlink(AppConfig.SHARE_LINK + account.getUsername() + "/" + id); textArea.setOnAction(action -> { try { @@ -390,12 +383,27 @@ private void handleShareButton(ActionEvent event) { } }); textArea.setWrapText(true); + showShareRecipe(textArea); + } + + private void showShareRecipe(Hyperlink textArea) { + String styleAlert = "-fx-background-color: #F1FFCB; -fx-font-weight: bold;"; + GridPane gridPane = new GridPane(); gridPane.setMaxWidth(Double.MAX_VALUE); gridPane.add(textArea, 0, 0); gridPane.setStyle(styleAlert); gridPane.setPrefSize(220, 220); + Button copyButton = new Button("Copy to Clipboard"); + copyButton.setOnAction(event -> { + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + content.putString(textArea.getText()); + clipboard.setContent(content); + }); + gridPane.add(copyButton, 0, 2); + Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Share this recipe!"); alert.setHeaderText("Share this recipe with a friend!"); @@ -415,6 +423,10 @@ private void handleGoToCreateLogin(ActionEvent event) { view.goToCreateAcc(); } + private void handleGoToLogin(ActionEvent event) { + view.goToLoginUI(); + } + private void handleRefreshButton(ActionEvent event) { // Get ChatGPT response from the Model List inputs = view.getAppFrameMic().getVoiceResponse(); @@ -451,7 +463,7 @@ private void handleCreateAcc(ActionEvent event) { if (username.isEmpty() || password.isEmpty()) { // Display an error message if username or password is empty showErrorPane(grid, "Error. Please provide a username and password."); - } else if (isUsernameTaken(username)) { + } else if (isUsernameTaken(username, password)) { // Display an error message if the username is already taken showErrorPane(grid, "Error. This username is already taken. Please choose another one."); } else { @@ -492,10 +504,10 @@ private void showSuccessPane(GridPane grid) { timeline.play(); } - private boolean isUsernameTaken(String username) { + private boolean isUsernameTaken(String username, String password) { // Check if the username is already taken // temporary logic, no database yet - String response = model.performAccountRequest("GET", username, ""); + String response = model.performAccountRequest("GET", username, password); // System.out.println("Response for usernameTaken : " + response); return (response.equals("Username is taken")); } diff --git a/app/src/main/java/code/client/Model/MockWhisperService.java b/app/src/main/java/code/client/Model/MockWhisperService.java index f9b890a..5e22924 100644 --- a/app/src/main/java/code/client/Model/MockWhisperService.java +++ b/app/src/main/java/code/client/Model/MockWhisperService.java @@ -14,7 +14,7 @@ public MockWhisperService(IHttpConnection connection) { public String processAudio(String type) throws IOException, URISyntaxException { if (type.equals("mealtype")) { - return "Breakfast"; + return "Breakfast."; } else if (type.equals("ingredients")) { return "Chicken, eggs."; } else { diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index 114b8a7..85f7f2d 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -20,7 +20,7 @@ public String performAccountRequest(String method, String user, String password) conn.setRequestMethod(method); conn.setDoOutput(true); System.out.println("Method is " + method); - + // make a new user if (method.equals("PUT")) { OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream()); @@ -28,7 +28,7 @@ public String performAccountRequest(String method, String user, String password) out.flush(); out.close(); } - + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String response = in.readLine(); in.close(); @@ -46,7 +46,7 @@ public String performRecipeRequest(String method, String recipe, String userId) if (userId != null) { urlString += "?=" + userId; } - + URL url = new URI(urlString).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); diff --git a/app/src/main/java/code/client/View/AccountCreationUI.java b/app/src/main/java/code/client/View/AccountCreationUI.java index c71e0b4..3aa52c6 100644 --- a/app/src/main/java/code/client/View/AccountCreationUI.java +++ b/app/src/main/java/code/client/View/AccountCreationUI.java @@ -4,9 +4,11 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; @@ -17,6 +19,7 @@ public class AccountCreationUI { private TextField usernameField; private PasswordField passwordField; private GridPane grid; + private Hyperlink goToLogin; AccountCreationUI() { grid = new GridPane(); @@ -43,7 +46,15 @@ public class AccountCreationUI { grid.add(passwordField, 1, 2); createAccountButton = new Button("Create Account"); + createAccountButton.setDefaultButton(true); grid.add(createAccountButton, 1, 3); + + goToLogin = new Hyperlink("Click to return to login."); + + FlowPane flow = new FlowPane(); + flow.getChildren().addAll( + new Text("Already have an account? "), goToLogin); + grid.add(flow, 1, 5); } public GridPane getRoot() { @@ -61,4 +72,8 @@ public PasswordField getPasswordField() { public void setCreateAccountButtonAction(EventHandler eventHandler) { createAccountButton.setOnAction(eventHandler); } + + public void setGoToLoginAction(EventHandler eventHandler) { + goToLogin.setOnAction(eventHandler); + } } diff --git a/app/src/main/java/code/client/View/AppFrameMic.java b/app/src/main/java/code/client/View/AppFrameMic.java index f9942b8..cea8f2c 100644 --- a/app/src/main/java/code/client/View/AppFrameMic.java +++ b/app/src/main/java/code/client/View/AppFrameMic.java @@ -242,18 +242,19 @@ private void recordMealType() { try { voiceToText = new MockWhisperService(); - mealType = voiceToText.processAudio("mealtype").toLowerCase(); + mealType = voiceToText.processAudio("mealtype").toUpperCase(); // type check - if (mealType.contains("breakfast")) { - mealTypeSelection.getMealType().setText("Breakfast"); - } else if (mealType.contains("lunch")) { - mealTypeSelection.getMealType().setText("Lunch"); - } else if (mealType.contains("dinner")) { - mealTypeSelection.getMealType().setText("Dinner"); + if (mealType.contains("BREAKFAST")) { + mealType = "Breakfast"; + } else if (mealType.contains("LUNCH")) { + mealType = "Lunch"; + } else if (mealType.contains("DINNER")) { + mealType = "Dinner"; } else { AppAlert.show("Input Error", "Please say a valid meal type!"); mealType = null; } + mealTypeSelection.getMealType().setText(mealType); } catch (IOException | URISyntaxException exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); exception.printStackTrace(); diff --git a/app/src/main/java/code/server/AccountRequestHandler.java b/app/src/main/java/code/server/AccountRequestHandler.java index 7a93596..b388aa9 100644 --- a/app/src/main/java/code/server/AccountRequestHandler.java +++ b/app/src/main/java/code/server/AccountRequestHandler.java @@ -51,7 +51,7 @@ private String handleGet(HttpExchange httpExchange) throws IOException { if (query != null) { String value = query.substring(query.indexOf("=") + 1); - + System.out.println("Value is: " + value); if (value != null) { String[] userNamePassword = value.split(":"); String username = userNamePassword[0]; From d9850516582fff977c18954c50818c6ba01602bb Mon Sep 17 00:00:00 2001 From: Allen Keng Date: Mon, 4 Dec 2023 12:40:23 -0800 Subject: [PATCH 02/31] chore: remove deprecated recipes.csv and credentials --- recipes.csv | 5 ----- userCredentials.csv | 1 - 2 files changed, 6 deletions(-) delete mode 100644 recipes.csv delete mode 100644 userCredentials.csv diff --git a/recipes.csv b/recipes.csv deleted file mode 100644 index 3e4be6d..0000000 --- a/recipes.csv +++ /dev/null @@ -1,5 +0,0 @@ -sep=:: -ID::Account::Title::Tag::Ingredients::Instructions::Image -107c7f79bcf86cd7994f6c0e::107c7f79bcf86cd7994f6c0e::Fried Chicken and Egg Fried Rice::Lunch::2 chicken breasts, diced;;2 large eggs;;2 cups cooked rice;;2 tablespoons vegetable oil::Heat the vegetable oil in a large pan over medium-high heat.::error -207c7f79bcf86cd7994f6c0e::207c7f79bcf86cd7994f6c0e::Testing::Breakfast::1 waffle;;2 bananas::Put them together;;Feast well.::error -307c7f79bcf86cd7994f6c0e::307c7f79bcf86cd7994f6c0e::Fried Chicken and Egg Fried Rice::Dinner::2 chicken breasts, diced;;2 large eggs;;2 cups cooked rice;;2 tablespoons vegetable oil::Heat the vegetable oil in a large pan over medium-high heat.::error diff --git a/userCredentials.csv b/userCredentials.csv deleted file mode 100644 index 474040f..0000000 --- a/userCredentials.csv +++ /dev/null @@ -1 +0,0 @@ -lol|lol|6566e0b0f7b6bc498444249c \ No newline at end of file From e4aeadf453be89688c0b1d34a59caa02bc6a21ea Mon Sep 17 00:00:00 2001 From: Allen Keng Date: Mon, 4 Dec 2023 12:41:31 -0800 Subject: [PATCH 03/31] docs: add project links and sample launch.json Co-authored-by: Samantha Prestrelski --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d64a4a..ddb5170 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,34 @@ # CSE 110 Project Team 39 Culinary Companion Crew PantryPals - the application that provides you with recipes that YOU have currently in your possession! +GitHub Repo: https://github.com/ucsd-cse110-fa23/cse-110-project-team-39 +MS1 Project Board: https://github.com/orgs/ucsd-cse110-fa23/projects/10 +MS2 Project Board: https://github.com/orgs/ucsd-cse110-fa23/projects/74 + ## How to run the app 1. Clone the repository 2. Change or create a `.vscode/launch.json`, and change the `vmArgs` to point to your JavaFX lib. -3. Run from `App.java`. \ No newline at end of file +3. Run from `App.java`. + +### Sample `launch.json` +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Current File", + "request": "launch", + "mainClass": "${file}" + }, + { + "type": "java", + "name": "App", + "request": "launch", + "mainClass": "code.App", + "projectName": "app", + "vmArgs": "--module-path 'user/javafx-sdk-21/lib' --add-modules javafx.controls,javafx.base,javafx.fxml,javafx.graphics,javafx.media,javafx.web --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.event=ALL-UNNAMED" + } + ] +} +``` \ No newline at end of file From e1c6eb5ba16802e42ab9670cd3216a29d3721d8c Mon Sep 17 00:00:00 2001 From: Allen Keng Date: Mon, 4 Dec 2023 15:38:21 -0800 Subject: [PATCH 04/31] Brought Whisper and ChatGPT to server. started End2End Test cases Co-authored-by: Samantha Prestrelski Co-authored-by: dashluu Co-authored-by: kjanderson1 Co-authored-by: ChristophianSulaiman Co-authored-by: Timoji --- .../code/client/Controllers/Controller.java | 255 ++++++++++++----- .../code/client/Model/AccountCSVReader.java | 35 +++ .../code/client/Model/AccountCSVWriter.java | 16 +- ...dioRecorder.java => AppAudioRecorder.java} | 18 +- .../java/code/client/Model/AppConfig.java | 8 + .../code/client/Model/BaseAudioRecorder.java | 20 ++ .../code/client/Model/MockAudioRecorder.java | 25 ++ .../code/client/Model/MockWhisperService.java | 4 +- .../main/java/code/client/Model/Model.java | 93 +++++++ .../java/code/client/Model/VoiceToText.java | 15 +- .../java/code/client/View/AppFrameMic.java | 259 +++--------------- .../code/client/View/DetailsAppFrame.java | 4 +- .../java/code/client/View/Ingredients.java | 82 ++++++ .../java/code/client/View/MealTagStyler.java | 28 ++ .../main/java/code/client/View/MealType.java | 82 ++++++ .../main/java/code/client/View/RecipeUI.java | 25 +- app/src/main/java/code/server/AppServer.java | 8 + .../code/server/ChatGPTRequestHandler.java | 87 ++++++ .../java/code/server/DallERequestHandler.java | 99 +++++++ .../server/MockChatGPTRequestHandler.java | 34 +++ .../code/server/MockDallERequestHandler.java | 70 +++++ .../server/MockWhisperRequestHandler.java | 28 ++ .../main/java/code/server/RecipeBuilder.java | 14 +- .../code/server/RecipeRequestHandler.java | 32 --- .../main/java/code/server/RecipeToImage.java | 11 + .../main/java/code/server/ShareRecipe.java | 101 +++++++ .../java/code/server/ShareRequestHandler.java | 96 +------ .../main/java/code/server/TextToRecipe.java | 24 ++ .../main/java/code/server/VoiceToText.java | 80 ++++++ .../main/java/code/server/WHISPERSERVER.java | 0 .../code/server/WhisperRequestHandler.java | 142 ++++++++++ app/src/test/java/code/AudioRecorderTest.java | 26 ++ app/src/test/java/code/AutologinTest.java | 46 +++- .../test/java/code/EndToEndScenario2_1.java | 120 ++++++++ .../test/java/code/EndToEndScenario2_2.java | 39 +++ app/src/test/java/code/RefreshRecipeTest.java | 5 - app/src/test/java/code/RefreshTest.java | 47 ++++ app/src/test/java/code/ShareRecipeTest.java | 69 +++++ 38 files changed, 1660 insertions(+), 487 deletions(-) create mode 100644 app/src/main/java/code/client/Model/AccountCSVReader.java rename app/src/main/java/code/client/Model/{AudioRecorder.java => AppAudioRecorder.java} (87%) create mode 100644 app/src/main/java/code/client/Model/BaseAudioRecorder.java create mode 100644 app/src/main/java/code/client/Model/MockAudioRecorder.java create mode 100644 app/src/main/java/code/client/View/Ingredients.java create mode 100644 app/src/main/java/code/client/View/MealTagStyler.java create mode 100644 app/src/main/java/code/client/View/MealType.java create mode 100644 app/src/main/java/code/server/ChatGPTRequestHandler.java create mode 100644 app/src/main/java/code/server/DallERequestHandler.java create mode 100644 app/src/main/java/code/server/MockChatGPTRequestHandler.java create mode 100644 app/src/main/java/code/server/MockDallERequestHandler.java create mode 100644 app/src/main/java/code/server/MockWhisperRequestHandler.java create mode 100644 app/src/main/java/code/server/RecipeToImage.java create mode 100644 app/src/main/java/code/server/ShareRecipe.java create mode 100644 app/src/main/java/code/server/TextToRecipe.java create mode 100644 app/src/main/java/code/server/VoiceToText.java create mode 100644 app/src/main/java/code/server/WHISPERSERVER.java create mode 100644 app/src/main/java/code/server/WhisperRequestHandler.java create mode 100644 app/src/test/java/code/AudioRecorderTest.java create mode 100644 app/src/test/java/code/EndToEndScenario2_1.java create mode 100644 app/src/test/java/code/EndToEndScenario2_2.java delete mode 100644 app/src/test/java/code/RefreshRecipeTest.java create mode 100644 app/src/test/java/code/RefreshTest.java create mode 100644 app/src/test/java/code/ShareRecipeTest.java diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index a3d4920..7daf227 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -2,10 +2,7 @@ import java.io.*; import java.net.URISyntaxException; -import java.util.Date; -import java.util.List; -import java.util.UUID; - +import java.util.*; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import code.client.View.RecipeListUI; @@ -15,28 +12,21 @@ import code.server.IRecipeDb; import java.net.URL; -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.animation.PauseTransition; -import javafx.animation.Timeline; +import javafx.animation.*; import javafx.collections.ObservableList; import javafx.event.ActionEvent; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.Hyperlink; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; +import javafx.geometry.Pos; +import javafx.scene.control.*; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.layout.GridPane; import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.scene.text.FontWeight; -import javafx.scene.text.Text; +import javafx.scene.text.*; import javafx.util.Duration; import code.client.Model.*; import code.client.View.AppAlert; import code.client.View.AppFrameHome; +import code.client.View.AppFrameMic; import code.client.View.DetailsAppFrame; import code.server.Recipe; @@ -64,6 +54,13 @@ public class Controller { private String defaultButtonStyle, onStyle, offStyle, blinkStyle; private String filter; + // Audio Stuff + private boolean recording; // keeps track if the app is currently recording + private final AppAudioRecorder recorder = new AppAudioRecorder(); + private VoiceToText voiceToText; + private String mealType; // stores the meal type specified by the user + private String ingredients; // stores the ingredients listed out by the user + public Controller(View view, Model model) { this.view = view; @@ -132,6 +129,11 @@ private void goToRecipeList() { MenuButton sortMenuButton = this.view.getAppFrameHome().getSortMenuButton(); setActiveState(filterMenuButton, 9); setActiveState(sortMenuButton, 9); + + RecipeListUI recipeListUI = this.view.getAppFrameHome().getRecipeList(); + RecipeSorter recipeSorter = new RecipeSorter(recipeListUI.getRecipeDB().getList()); + recipeSorter.sortNewestToOldest(); + sortList(sortMenuButton, NEWEST_TO_OLDEST_INDEX); // 9 is beyond the scope of menuItem indexes } @@ -157,17 +159,19 @@ private void displayUserRecipes() { private void handleNewButton(ActionEvent event) throws URISyntaxException, IOException { view.goToAudioCapture(); - this.view.getAppFrameMic().setGoToDetailedButtonAction(this::handleDetailedViewFromNewRecipeButton); - this.view.getAppFrameMic().setGoToHomeButtonAction(this::handleHomeButton); - } + AppFrameMic mic = this.view.getAppFrameMic(); + mic.setGoToDetailedButtonAction(this::handleDetailedViewFromNewRecipeButton); + mic.setGoToHomeButtonAction(this::handleHomeButton); + mic.setRecordIngredientsButtonAction(this::handleRecordIngredients); + mic.setRecordMealTypeButtonAction(event1 -> { + try { + handleRecordMealType(event1); + } catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } + }); - // private void handleSortButton(ActionEvent event) { - // ChoiceBox sortChoiceBox = view.getAppFrameHome().getSortChoiceBox(); - // RecipeListUI list = view.getAppFrameHome().getRecipeList(); - // view.getAppFrameHome().getSortButton().setOnAction(e -> { - // sortChoiceBox.show(); - // }); - // } + } private void addFilterListeners() { MenuButton filterMenuButton = this.view.getAppFrameHome().getFilterMenuButton(); @@ -287,28 +291,19 @@ public void addListenersToList() { private void handleDetailedViewFromNewRecipeButton(ActionEvent event) { // Get ChatGPT response from the Model - List inputs = view.getAppFrameMic().getVoiceResponse(); - - String mealType = inputs.get(0); - String ingredients = inputs.get(1); if (mealType != null && ingredients != null) { - TextToRecipe caller = new MockGPTService();// new ChatGPTService(); - RecipeToImage imageCaller = new MockDallEService(); // new DallEService(); - try { - String audioOutput1 = mealType; - String audioOutput2 = ingredients;// audio.processAudio(); - String responseText = caller.getResponse(audioOutput1, audioOutput2); - Recipe chatGPTrecipe = caller.mapResponseToRecipe(mealType, responseText); + String responseText = model.performChatGPTRequest("GET", mealType, ingredients); + Recipe chatGPTrecipe = mapResponseToRecipe(mealType, responseText); chatGPTrecipe.setAccountId(account.getId()); - chatGPTrecipe.setImage(imageCaller.getResponse(chatGPTrecipe.getTitle())); + chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); // Changes UI to Detailed Recipe Screen view.goToDetailedView(chatGPTrecipe, false); view.getDetailedView().getRecipeDetailsUI().setEditable(false); handleDetailedViewListeners(); - } catch (IOException | URISyntaxException | InterruptedException exception) { + } catch (Exception exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); exception.printStackTrace(); } @@ -387,14 +382,15 @@ private void handleShareButton(ActionEvent event) { } private void showShareRecipe(Hyperlink textArea) { - String styleAlert = "-fx-background-color: #F1FFCB; -fx-font-weight: bold;"; + String styleAlert = "-fx-background-color: #F1FFCB; -fx-font-weight: bold; -fx-font: 14 arial"; GridPane gridPane = new GridPane(); gridPane.setMaxWidth(Double.MAX_VALUE); gridPane.add(textArea, 0, 0); gridPane.setStyle(styleAlert); gridPane.setPrefSize(220, 220); - + gridPane.setAlignment(Pos.TOP_CENTER); + textArea.setTextAlignment(TextAlignment.CENTER); Button copyButton = new Button("Copy to Clipboard"); copyButton.setOnAction(event -> { Clipboard clipboard = Clipboard.getSystemClipboard(); @@ -402,7 +398,7 @@ private void showShareRecipe(Hyperlink textArea) { content.putString(textArea.getText()); clipboard.setContent(content); }); - gridPane.add(copyButton, 0, 2); + gridPane.add(copyButton, 0, 3); Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Share this recipe!"); @@ -429,27 +425,22 @@ private void handleGoToLogin(ActionEvent event) { private void handleRefreshButton(ActionEvent event) { // Get ChatGPT response from the Model - List inputs = view.getAppFrameMic().getVoiceResponse(); - - String mealType = inputs.get(0); - String ingredients = inputs.get(1); + if (mealType != null && ingredients != null) { + try { + String responseText = model.performChatGPTRequest("GET", mealType, ingredients); + Recipe chatGPTrecipe = mapResponseToRecipe(mealType, responseText); + chatGPTrecipe.setAccountId(account.getId()); + chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); - TextToRecipe caller = new MockGPTService();// new ChatGPTService(); - RecipeToImage imageCaller = new MockDallEService(); // new DallEService(); + // Changes UI to Detailed Recipe Screen + view.getDetailedView().setRecipe(chatGPTrecipe); - try { - String audioOutput1 = mealType; - String audioOutput2 = ingredients;// audio.processAudio(); - String responseText = caller.getResponse(audioOutput1, audioOutput2); - Recipe chatGPTrecipe = caller.mapResponseToRecipe(mealType, responseText); - chatGPTrecipe.setAccountId(account.getId()); - chatGPTrecipe.setImage(imageCaller.getResponse(chatGPTrecipe.getTitle())); - - // Changes UI to Detailed Recipe Screen - view.getDetailedView().setRecipe(chatGPTrecipe); - } catch (IOException | URISyntaxException | InterruptedException exception) { - AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); - exception.printStackTrace(); + } catch (Exception exception) { + AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); + exception.printStackTrace(); + } + } else { + AppAlert.show("Input Error", "Invalid meal type or ingredients, please try again!"); } } @@ -613,5 +604,145 @@ private boolean performLogin(String username, String password) { account = new Account(accountId, username, password); return true; } - /////////////////////////////// + + /////////////////////////////// AUDIOMANAGEMENT/////////////////////////////////// + public void handleRecordMealType(ActionEvent event) throws IOException, URISyntaxException { + recordMealType(); + } + + public void handleRecordIngredients(ActionEvent event) { + recordIngredients(); + }; + + private void recordMealType() { + AppFrameMic mic = this.view.getAppFrameMic(); + if (!recording) { + recorder.startRecording(); + recording = true; + mic.getRecordMealTypeButton().setStyle("-fx-background-color: #FF0000;"); + mic.getRecordingMealTypeLabel().setVisible(true); + // recordingLabel1.setStyle("-fx-font-color: #FF0000;"); + } else { + recorder.stopRecording(); + recording = false; + mic.getRecordMealTypeButton().setStyle(""); + mic.getRecordingMealTypeLabel().setVisible(false); + // recordingLabel1.setStyle(""); + + try { + mealType = model.performWhisperRequest("GET", "mealType"); + if (mealType == null) { + AppAlert.show("Input Error", "Please say a valid meal type!"); + } + mic.getMealBox().getMealType().setText(mealType); + } catch (IOException exception) { + AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); + exception.printStackTrace(); + } + } + } + + private void recordIngredients() { + AppFrameMic mic = this.view.getAppFrameMic(); + if (!recording) { + recorder.startRecording(); + recording = true; + mic.getRecordIngredientsButton().setStyle("-fx-background-color: #FF0000;"); + mic.getRecordingIngredientsLabel().setVisible(true); + // recordingLabel2.setStyle("-fx-background-color: #FF0000;"); + } else { + recorder.stopRecording(); + recording = false; + mic.getRecordIngredientsButton().setStyle(""); + mic.getRecordingIngredientsLabel().setVisible(false); + // recordingLabel2.setStyle(""); + + try { + mealType = model.performWhisperRequest("GET", "ingredients"); + String nonAsciiCharactersRegex = "[^\\x00-\\x7F]"; + + if (ingredients.matches(".*" + nonAsciiCharactersRegex + ".*") || + ingredients.trim().isEmpty() || + ingredients.contains("you")) { + AppAlert.show("Input Error", "Please provide valid ingredients!"); + ingredients = null; + } else { + mic.getIngredBox().getIngredients().setText(ingredients); + } + } catch (IOException exception) { + AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); + exception.printStackTrace(); + } + } + } + + /////////////////////////////// AUDIOMANAGEMENT////////////////////////////////// + + /* + * TODO: move this to a separate class + */ + public Recipe mapResponseToRecipe(String mealType, String responseText) { + // Split the tokens into lines + String[] tokenArr = responseText.split("\n"); + List tokenList = new ArrayList<>(Arrays.asList(tokenArr)); + int i; + + // Remove empty tokens + for (i = 0; i < tokenList.size();) { + if (tokenList.get(i).isBlank()) { + tokenList.remove(i); + } else { + ++i; + } + } + + // Create a new recipe with a title + Recipe recipe = new Recipe(tokenList.get(0), mealType); + + // Parse recipe's ingredients + String ingredient; + boolean parse = false; + for (i = 0; !tokenList.get(i).contains("Instructions"); ++i) { + ingredient = tokenList.get(i).trim(); + if (ingredient.contains("Ingredients")) { + parse = true; + } else if (parse) { + ingredient = removeDashFromIngredient(tokenList.get(i).trim()); + recipe.addIngredient(ingredient); + } + } + + // Parse recipe's instructions + String instruction; + for (i += 1; i < tokenList.size(); ++i) { + instruction = removeNumberFromInstruction(tokenList.get(i).trim()); + recipe.addInstruction(instruction); + } + + return recipe; + } + + private String removeDashFromIngredient(String ingredient) { + if (ingredient.charAt(0) == ('-')) { + return ingredient.substring(1).trim(); + } + return ingredient.substring(2); + } + + private String removeNumberFromInstruction(String instruction) { + StringBuilder strBuilder = new StringBuilder(); + int i; + + // Ignore characters until '.' + for (i = 0; i < instruction.length() && (instruction.charAt(i) != '.'); ++i) + ; + + // Ignore '.' and ' ' after + // Get all characters until end of string + for (i += 2; i < instruction.length(); ++i) { + strBuilder.append(instruction.charAt(i)); + } + + return strBuilder.toString(); + } } \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/AccountCSVReader.java b/app/src/main/java/code/client/Model/AccountCSVReader.java new file mode 100644 index 0000000..4379194 --- /dev/null +++ b/app/src/main/java/code/client/Model/AccountCSVReader.java @@ -0,0 +1,35 @@ +package code.client.Model; + +import java.util.List; +import java.util.ArrayList; +import java.io.IOException; +import java.io.FileReader; +import java.io.BufferedReader; + +public class AccountCSVReader { + private final BufferedReader reader; + + public AccountCSVReader(FileReader fileReader) { + this.reader = new BufferedReader(fileReader); + } + + public List readUserCredentials() throws IOException { + // Helper variable to store a line of the user credentials file + String line; + // Helper variable to store the return value and store user credentials + List toReturn = new ArrayList<>(); + // Helper variable to store the username and password in the csv file + String[] userCredentials; + // Read the lines of the UserCredentials.csv file + while ((line = reader.readLine()) != null) { + userCredentials = line.split("\\|"); + toReturn.add(userCredentials[0]); + toReturn.add(userCredentials[1]); + } + return toReturn; + } + + public void close() throws IOException { + reader.close(); + } +} \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/AccountCSVWriter.java b/app/src/main/java/code/client/Model/AccountCSVWriter.java index 19ef45b..8e49cb4 100644 --- a/app/src/main/java/code/client/Model/AccountCSVWriter.java +++ b/app/src/main/java/code/client/Model/AccountCSVWriter.java @@ -1,18 +1,22 @@ package code.client.Model; import java.io.IOException; -import java.io.Writer; +import java.io.FileWriter; public class AccountCSVWriter { - private final Writer writer; + private final FileWriter writer; - public AccountCSVWriter(Writer writer) { + public AccountCSVWriter(FileWriter writer) { this.writer = writer; } public void writeAccount(String username, String password) throws IOException { - writer.append(username) - .append("|") - .append(password); + writer.write(username); + writer.write("|"); + writer.write(password); + } + + public void close() throws IOException { + writer.close(); } } diff --git a/app/src/main/java/code/client/Model/AudioRecorder.java b/app/src/main/java/code/client/Model/AppAudioRecorder.java similarity index 87% rename from app/src/main/java/code/client/Model/AudioRecorder.java rename to app/src/main/java/code/client/Model/AppAudioRecorder.java index 37bc90d..33330a5 100644 --- a/app/src/main/java/code/client/Model/AudioRecorder.java +++ b/app/src/main/java/code/client/Model/AppAudioRecorder.java @@ -3,12 +3,11 @@ import java.io.*; import javax.sound.sampled.*; -public class AudioRecorder { +public class AppAudioRecorder extends BaseAudioRecorder { private AudioFormat audioFormat; private TargetDataLine targetDataLine; - private boolean recording; - public AudioRecorder() { + public AppAudioRecorder() { this.audioFormat = getAudioFormat(); } @@ -45,6 +44,7 @@ private void recordOnThread() { } } + @Override public void startRecording() { Thread thread = new Thread( new Runnable() { @@ -56,19 +56,9 @@ public void run() { thread.start(); } + @Override public void stopRecording() { targetDataLine.stop(); targetDataLine.close(); } - - public boolean toggleRecording() { - if (recording) { - stopRecording(); - } else { - startRecording(); - } - - recording = !recording; - return recording; - } } diff --git a/app/src/main/java/code/client/Model/AppConfig.java b/app/src/main/java/code/client/Model/AppConfig.java index 9f97854..02519d2 100644 --- a/app/src/main/java/code/client/Model/AppConfig.java +++ b/app/src/main/java/code/client/Model/AppConfig.java @@ -6,21 +6,29 @@ public class AppConfig { public static final String APP_NAME = "Pantry Pal"; public static final String RECIPE_CSV_FILE = "recipes.csv"; public static final String CREDENTIALS_CSV_FILE = "userCredentials.csv"; + // API public static final String AUDIO_FILE = "recording.wav"; public static final AudioFileFormat.Type AUDIO_TYPE = AudioFileFormat.Type.WAVE; public static final String API_KEY = "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"; + // images public static final String MICROPHONE_IMG_FILE = "app/src/main/java/code/client/View/microphone.png"; public static final String OFFLINE_IMG_FILE = "app/src/main/java/code/client/View/cat.png"; public static final String RECIPE_IMG_FILE = "app/src/main/java/code/client/View/defaultRecipe.png"; + // mongo public static final String MONGODB_CONN = "mongodb://trungluu:xGoGkkbozvWyiXyZ@ac-ajwebab-shard-00-00.lta1oi1.mongodb.net:27017,ac-ajwebab-shard-00-01.lta1oi1.mongodb.net:27017,ac-ajwebab-shard-00-02.lta1oi1.mongodb.net:27017/?ssl=true&replicaSet=atlas-3daxhg-shard-0&authSource=admin&retryWrites=true&w=majority"; public static final String MONGO_DB = "pantry_pal"; public static final String MONGO_RECIPE_COLLECTION = "recipes"; public static final String MONGO_USER_COLLECTION = "users"; + // server public static final String SERVER_HOST = "localhost"; public static final int SERVER_PORT = 8100; public static final String SERVER_URL = "http://" + SERVER_HOST + ":" + SERVER_PORT; public static final String RECIPE_PATH = "/recipe"; public static final String ACCOUNT_PATH = "/accounts"; + public static final String CHATGPT_PATH = "/chatgpt"; + public static final String DALLE_PATH = "/dalle"; + public static final String WHISPER_PATH = "/whisper"; + // sharing public static final String SHARE_PATH = "/recipes/"; public static final String SHARE_LINK = "http://localhost:8100/recipes/"; } diff --git a/app/src/main/java/code/client/Model/BaseAudioRecorder.java b/app/src/main/java/code/client/Model/BaseAudioRecorder.java new file mode 100644 index 0000000..4452d49 --- /dev/null +++ b/app/src/main/java/code/client/Model/BaseAudioRecorder.java @@ -0,0 +1,20 @@ +package code.client.Model; + +public abstract class BaseAudioRecorder { + protected boolean recording; + + public abstract void startRecording(); + + public abstract void stopRecording(); + + public boolean toggleRecording() { + if (recording) { + stopRecording(); + } else { + startRecording(); + } + + recording = !recording; + return recording; + } +} \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/MockAudioRecorder.java b/app/src/main/java/code/client/Model/MockAudioRecorder.java new file mode 100644 index 0000000..43b5e76 --- /dev/null +++ b/app/src/main/java/code/client/Model/MockAudioRecorder.java @@ -0,0 +1,25 @@ +package code.client.Model; + +import java.io.IOException; +import java.io.Writer; + +public class MockAudioRecorder extends BaseAudioRecorder { + private Writer writer; + + public MockAudioRecorder(Writer writer) { + this.writer = writer; + } + + @Override + public void startRecording() { + } + + @Override + public void stopRecording() { + try { + writer.write("Recipe recorded."); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/code/client/Model/MockWhisperService.java b/app/src/main/java/code/client/Model/MockWhisperService.java index 5e22924..034351c 100644 --- a/app/src/main/java/code/client/Model/MockWhisperService.java +++ b/app/src/main/java/code/client/Model/MockWhisperService.java @@ -14,12 +14,12 @@ public MockWhisperService(IHttpConnection connection) { public String processAudio(String type) throws IOException, URISyntaxException { if (type.equals("mealtype")) { - return "Breakfast."; + // processed correctly + return "Breakfast"; } else if (type.equals("ingredients")) { return "Chicken, eggs."; } else { return "Error text"; } } - } diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index 85f7f2d..41c1507 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -1,11 +1,18 @@ package code.client.Model; import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URL; +import java.net.URLConnection; import java.net.URI; +import java.nio.file.*; public class Model { public String performAccountRequest(String method, String user, String password) { @@ -72,4 +79,90 @@ public String performRecipeRequest(String method, String recipe, String userId) return "Error: " + ex.getMessage(); } } + + public String performChatGPTRequest(String method, String mealType, String ingredients) { + try { + String urlString = AppConfig.SERVER_URL + AppConfig.CHATGPT_PATH; + urlString += "?=" + mealType + "::" + ingredients; + URL url = new URI(urlString).toURL(); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setDoOutput(true); + // System.out.println("Method is " + method); + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String response = in.readLine(); + in.close(); + return response; + } catch (Exception ex) { + ex.printStackTrace(); + return "Error: " + ex.getMessage(); + } + } + + public String performDallERequest(String method, String recipeTitle) { + try { + String urlString = AppConfig.SERVER_URL + AppConfig.CHATGPT_PATH; + urlString += "?=" + recipeTitle; + URL url = new URI(urlString).toURL(); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setDoOutput(true); + // System.out.println("Method is " + method); + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String response = in.readLine(); + in.close(); + return response; + } catch (Exception ex) { + ex.printStackTrace(); + return "Error: " + ex.getMessage(); + } + } + + public String performWhisperRequest(String method, String type) throws MalformedURLException, IOException { + String response = "Unable to perform Whisper request"; + final String postUrl = AppConfig.SERVER_URL + AppConfig.WHISPER_PATH; + final File audioFile = new File(AppConfig.AUDIO_FILE); + String boundary = Long.toHexString(System.currentTimeMillis()); + String CRLF = "\r\n"; + String charset = "UTF-8"; + URLConnection connection = new URL(postUrl).openConnection(); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + + try (OutputStream output = connection.getOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);) { + writer.append("--" + boundary).append(CRLF); + writer.append( + "Content-Disposition: form-data; name=\"binaryFile\"; filename=\"" + audioFile.getName() + "\"") + .append(CRLF); + writer.append("Content-Length: " + audioFile.length()).append(CRLF); + writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(audioFile.getName())).append(CRLF); + writer.append("Content-Transfer-Encoding: binary").append(CRLF); + writer.append(CRLF).flush(); + Files.copy(audioFile.toPath(), output); + output.flush(); + int responseCode = ((HttpURLConnection) connection).getResponseCode(); + response = ((HttpURLConnection) connection).getResponseMessage(); + System.out.println("Response code: [" + responseCode + "]"); + + if (type.equals("mealType") && responseCode == 200) { + response = response.toUpperCase(); + if (response.contains("BREAKFAST")) { + response = "Breakfast"; + } else if (response.contains("LUNCH")) { + response = "Lunch"; + } else if (response.contains("DINNER")) { + response = "Dinner"; + } else { + response = null; + } + } + } + + return response; + } } \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/VoiceToText.java b/app/src/main/java/code/client/Model/VoiceToText.java index 726fc71..457b0d0 100644 --- a/app/src/main/java/code/client/Model/VoiceToText.java +++ b/app/src/main/java/code/client/Model/VoiceToText.java @@ -15,7 +15,20 @@ public VoiceToText(IHttpConnection connection) { } public String processAudio(String type) throws IOException, URISyntaxException { - return handleResponse(); + String response = handleResponse(); + if (type.equals("mealtype")) { + response = response.toUpperCase(); + if (response.contains("BREAKFAST")) { + response = "Breakfast"; + } else if (response.contains("LUNCH")) { + response = "Lunch"; + } else if (response.contains("DINNER")) { + response = "Dinner"; + } else { + response = null; + } + } + return response; } private String handleResponse() throws IOException { diff --git a/app/src/main/java/code/client/View/AppFrameMic.java b/app/src/main/java/code/client/View/AppFrameMic.java index cea8f2c..eb21593 100644 --- a/app/src/main/java/code/client/View/AppFrameMic.java +++ b/app/src/main/java/code/client/View/AppFrameMic.java @@ -15,138 +15,10 @@ import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.text.*; +import code.client.View.Ingredients; +import code.client.View.MealType; // import javax.sound.sampled.*; -class MealTypeSelection extends GridPane { - private final Label prompt, recordingLabel; - private final Button recordButton; - private final ImageView microphone; - private final TextArea mealTypeArea; - - // =================FIRST PROMPT=================// - MealTypeSelection() { - // Set the preferred vertical and horizontal gaps - this.setVgap(20); - this.setHgap(20); - - // Get a picture of a microphone for the voice recording button - File file = new File(AppConfig.MICROPHONE_IMG_FILE); - microphone = new ImageView(new Image(file.toURI().toString())); - - // Set the size of the microphone image - microphone.setFitWidth(50); - microphone.setFitHeight(50); - microphone.setScaleX(1); - microphone.setScaleY(1); - - // Create a recording button - recordButton = new Button(); - recordButton.setGraphic(microphone); - - // Create a label that indicated if the app is currently recording - recordingLabel = new Label("Recording..."); - recordingLabel.setTextFill(Color.web("#FF0000")); - recordingLabel.setVisible(false); - - // Set the user prompt for meal type selection - prompt = new Label("Select Meal Type (Breakfast, Lunch, or Dinner)"); - prompt.setStyle("-fx-font-size: 16; -fx-font-weight: bold;"); - // prompt.setTextFill(Color.web("#FF0000")); - - // Set a textField for the meal type that was selected - mealTypeArea = new TextArea(); - mealTypeArea.setPromptText("Meal Type"); - mealTypeArea.setStyle("-fx-font-size: 16"); // CHANGE 1 (FONT) - mealTypeArea.setPrefWidth(300); - mealTypeArea.setPrefHeight(50); - mealTypeArea.setEditable(true); - - // Add all of the elements to the MealTypeSelection - this.add(recordButton, 0, 0); - this.add(recordingLabel, 0, 1); - this.add(prompt, 0, 2); - this.add(mealTypeArea, 0, 3); - - } - - public TextArea getMealType() { - return mealTypeArea; - } - - public Button getRecordButton() { - return recordButton; - } - - public Label getRecordingLabel() { - return recordingLabel; - } -} - -class IngredientsList extends GridPane { - - private Label prompt, recordingLabel; - private Button recordButton; - private ImageView microphone; - private TextArea ingredientsArea; - - // ==============SECOND PROMPT=================// - IngredientsList() { - // Set the preferred vertical and horizontal gaps - this.setVgap(20); - this.setHgap(20); - - // Get a picture of a microphone for the voice recording button - File file = new File("app/src/main/java/code/client/View/microphone.png"); - microphone = new ImageView(new Image(file.toURI().toString())); - - // Set the size of the microphone image - microphone.setFitWidth(50); - microphone.setFitHeight(50); - microphone.setScaleX(1); - microphone.setScaleY(1); - - // Create a recording button - recordButton = new Button(); - recordButton.setGraphic(microphone); - - // Create a label that indicated if the app is currently recording - recordingLabel = new Label("Recording..."); - recordingLabel.setTextFill(Color.web("#FF0000")); - recordingLabel.setVisible(false); - - // Set the user prompt for meal type selection - prompt = new Label("Please List Your Ingredients"); - prompt.setStyle("-fx-font-size: 16; -fx-font-weight: bold;"); - // prompt.setTextFill(Color.web("#FF0000")); - - // Set a textField for the meal type that was selected - ingredientsArea = new TextArea(); - ingredientsArea.setPromptText("Ingredients"); - ingredientsArea.setStyle("-fx-font-size: 16"); // change - ingredientsArea.setPrefWidth(300); // CHANGE 3 (WIDTH OF PROMPT) - ingredientsArea.setPrefHeight(50); // CHANGE - ingredientsArea.setEditable(true); - - // Add all of the elements to the MealTypeSelection - this.add(recordButton, 0, 0); - this.add(recordingLabel, 0, 1); - this.add(prompt, 0, 2); - this.add(ingredientsArea, 0, 3); - } - - public TextArea getIngredients() { - return ingredientsArea; - } - - public Button getRecordButton() { - return recordButton; - } - - public Label getRecordingLabel() { - return recordingLabel; - } -} - class GPTRecipe extends GridPane { private Label recipeLabel; private TextField recipeField; @@ -178,25 +50,20 @@ class HeaderMic extends HBox { public class AppFrameMic extends BorderPane { // Helper variables for button functionality - private boolean recording; // keeps track if the app is currently recording - private String mealType; // stores the meal type specified by the user - private String ingredients; // stores the ingredients listed out by the user // AppFrameMic elements private GridPane recipeCreationGrid; private HeaderMic header; - private MealTypeSelection mealTypeSelection; - private IngredientsList ingredientsList; + private MealType mealTypeSelection; + private Ingredients ingredientsList; private Button recordMealTypeButton, recordIngredientsButton, goToDetailedButton, backButton; private Label recordingMealTypeLabel, recordingIngredientsLabel; - private final AudioRecorder recorder = new AudioRecorder(); - private VoiceToText voiceToText; AppFrameMic() throws URISyntaxException, IOException { setStyle("-fx-background-color: #DAE5EA;"); // If want to change // background color header = new HeaderMic(); - mealTypeSelection = new MealTypeSelection(); - ingredientsList = new IngredientsList(); + mealTypeSelection = new MealType(); + ingredientsList = new Ingredients(); recipeCreationGrid = new GridPane(); recipeCreationGrid.setAlignment(Pos.CENTER); @@ -223,104 +90,50 @@ public class AppFrameMic extends BorderPane { backButton = new Button("Back to List"); recipeCreationGrid.add(backButton, 0, 0); - addListeners(); } - private void recordMealType() { - if (!recording) { - recorder.startRecording(); - recording = true; - recordMealTypeButton.setStyle("-fx-background-color: #FF0000;"); - recordingMealTypeLabel.setVisible(true); - // recordingLabel1.setStyle("-fx-font-color: #FF0000;"); - } else { - recorder.stopRecording(); - recording = false; - recordMealTypeButton.setStyle(""); - recordingMealTypeLabel.setVisible(false); - // recordingLabel1.setStyle(""); - - try { - voiceToText = new MockWhisperService(); - mealType = voiceToText.processAudio("mealtype").toUpperCase(); - // type check - if (mealType.contains("BREAKFAST")) { - mealType = "Breakfast"; - } else if (mealType.contains("LUNCH")) { - mealType = "Lunch"; - } else if (mealType.contains("DINNER")) { - mealType = "Dinner"; - } else { - AppAlert.show("Input Error", "Please say a valid meal type!"); - mealType = null; - } - mealTypeSelection.getMealType().setText(mealType); - } catch (IOException | URISyntaxException exception) { - AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); - exception.printStackTrace(); - } - } + public void setGoToDetailedButtonAction(EventHandler eventHandler) { + goToDetailedButton.setOnAction(eventHandler); } - private void recordIngredients() { - if (!recording) { - recorder.startRecording(); - recording = true; - recordIngredientsButton.setStyle("-fx-background-color: #FF0000;"); - recordingIngredientsLabel.setVisible(true); - // recordingLabel2.setStyle("-fx-background-color: #FF0000;"); - } else { - recorder.stopRecording(); - recording = false; - recordIngredientsButton.setStyle(""); - recordingIngredientsLabel.setVisible(false); - // recordingLabel2.setStyle(""); + public void setGoToHomeButtonAction(EventHandler eventHandler) { + backButton.setOnAction(eventHandler); + } - try { - voiceToText = new MockWhisperService(); - ingredients = voiceToText.processAudio("ingredients"); - String nonAsciiCharactersRegex = "[^\\x00-\\x7F]"; + public void setRecordMealTypeButtonAction(EventHandler eventHandler) { + recordMealTypeButton.setOnAction(eventHandler); + } - if (ingredients.matches(".*" + nonAsciiCharactersRegex + ".*") || - ingredients.trim().isEmpty() || - ingredients.contains("you")) { - AppAlert.show("Input Error", "Please provide valid ingredients!"); - ingredients = null; - } else { - ingredientsList.getIngredients().setText(ingredients); - } - } catch (IOException | URISyntaxException exception) { - AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); - exception.printStackTrace(); - } - } + public void setRecordIngredientsButtonAction(EventHandler eventHandler) { + recordIngredientsButton.setOnAction(eventHandler); } - public void addListeners() throws IOException, URISyntaxException { - recordMealTypeButton.setOnAction(event -> { - recordMealType(); - }); + public Button getRecordMealTypeButton() { + return recordMealTypeButton; + } - recordIngredientsButton.setOnAction(event -> { - recordIngredients(); - }); + public Button getRecordIngredientsButton() { + return recordIngredientsButton; + } + public Label getMealTypeLabel() { + return recordingMealTypeLabel; } - public void setGoToDetailedButtonAction(EventHandler eventHandler) { - goToDetailedButton.setOnAction(eventHandler); + public Label getRecordingIngredientsLabel() { + return recordingIngredientsLabel; } - public void setGoToHomeButtonAction(EventHandler eventHandler) { - backButton.setOnAction(eventHandler); + public Label getRecordingMealTypeLabel() { + return recordingMealTypeLabel; } - public Button getRecordMealTypeButton() { - return recordMealTypeButton; + public MealType getMealBox() { + return mealTypeSelection; } - public Button getRecordIngredientsButton() { - return recordIngredientsButton; + public Ingredients getIngredBox() { + return ingredientsList; } public StackPane getRoot() { @@ -328,12 +141,4 @@ public StackPane getRoot() { stack.getChildren().add(this); return stack; } - - public ArrayList getVoiceResponse() { - ArrayList temp = new ArrayList<>(); - temp.add(mealType); - temp.add(ingredients); - return temp; - } - } diff --git a/app/src/main/java/code/client/View/DetailsAppFrame.java b/app/src/main/java/code/client/View/DetailsAppFrame.java index bff476d..ad81cc0 100644 --- a/app/src/main/java/code/client/View/DetailsAppFrame.java +++ b/app/src/main/java/code/client/View/DetailsAppFrame.java @@ -136,7 +136,9 @@ public void updateDisplay() { HBox topButtons = new HBox(); topButtons.setSpacing(100); topButtons.setAlignment(Pos.CENTER); - topButtons.getChildren().addAll(backToHomeButton, refreshButton); + Button mealTag = new Button(); + MealTagStyler.styleTags(currentRecipe, mealTag); + topButtons.getChildren().addAll(backToHomeButton, mealTag, refreshButton); HBox.setHgrow(topButtons, Priority.ALWAYS); detailedUI.getChildren().addAll(topButtons, title, recipeImgView); diff --git a/app/src/main/java/code/client/View/Ingredients.java b/app/src/main/java/code/client/View/Ingredients.java new file mode 100644 index 0000000..170f7e3 --- /dev/null +++ b/app/src/main/java/code/client/View/Ingredients.java @@ -0,0 +1,82 @@ +package code.client.View; + +import code.client.Model.*; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; + +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.text.*; + +public class Ingredients extends GridPane { + + private Label prompt, recordingLabel; + private Button recordButton; + private ImageView microphone; + private TextArea ingredientsArea; + + // ==============SECOND PROMPT=================// + Ingredients() { + // Set the preferred vertical and horizontal gaps + this.setVgap(20); + this.setHgap(20); + + // Get a picture of a microphone for the voice recording button + File file = new File("app/src/main/java/code/client/View/microphone.png"); + microphone = new ImageView(new Image(file.toURI().toString())); + + // Set the size of the microphone image + microphone.setFitWidth(50); + microphone.setFitHeight(50); + microphone.setScaleX(1); + microphone.setScaleY(1); + + // Create a recording button + recordButton = new Button(); + recordButton.setGraphic(microphone); + + // Create a label that indicated if the app is currently recording + recordingLabel = new Label("Recording..."); + recordingLabel.setTextFill(Color.web("#FF0000")); + recordingLabel.setVisible(false); + + // Set the user prompt for meal type selection + prompt = new Label("Please List Your Ingredients"); + prompt.setStyle("-fx-font-size: 16; -fx-font-weight: bold;"); + // prompt.setTextFill(Color.web("#FF0000")); + + // Set a textField for the meal type that was selected + ingredientsArea = new TextArea(); + ingredientsArea.setPromptText("Ingredients"); + ingredientsArea.setStyle("-fx-font-size: 16"); // change + ingredientsArea.setPrefWidth(300); // CHANGE 3 (WIDTH OF PROMPT) + ingredientsArea.setPrefHeight(50); // CHANGE + ingredientsArea.setEditable(true); + + // Add all of the elements to the MealTypeSelection + this.add(recordButton, 0, 0); + this.add(recordingLabel, 0, 1); + this.add(prompt, 0, 2); + this.add(ingredientsArea, 0, 3); + } + + public TextArea getIngredients() { + return ingredientsArea; + } + + public Button getRecordButton() { + return recordButton; + } + + public Label getRecordingLabel() { + return recordingLabel; + } +} diff --git a/app/src/main/java/code/client/View/MealTagStyler.java b/app/src/main/java/code/client/View/MealTagStyler.java new file mode 100644 index 0000000..d27791e --- /dev/null +++ b/app/src/main/java/code/client/View/MealTagStyler.java @@ -0,0 +1,28 @@ +package code.client.View; + +import javafx.scene.control.Button; +import code.server.Recipe; + +public class MealTagStyler { + public static void styleTags(Recipe recipe, Button mealType) { + switch (recipe.getMealTag().toLowerCase()) { + case "breakfast": + mealType.setStyle( + "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #FF7276; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); + mealType.setText("Breakfast"); + break; + + case "lunch": + mealType.setStyle( + "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FFFF; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); + mealType.setText("Lunch"); + break; + + case "dinner": + mealType.setStyle( + "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FF00; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); + mealType.setText("Dinner"); + break; + } + } +} diff --git a/app/src/main/java/code/client/View/MealType.java b/app/src/main/java/code/client/View/MealType.java new file mode 100644 index 0000000..778f657 --- /dev/null +++ b/app/src/main/java/code/client/View/MealType.java @@ -0,0 +1,82 @@ +package code.client.View; + +import code.client.Model.*; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; + +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.text.*; + +public class MealType extends GridPane { + private final Label prompt, recordingLabel; + private final Button recordButton; + private final ImageView microphone; + private final TextArea mealTypeArea; + + // =================FIRST PROMPT=================// + MealType() { + // Set the preferred vertical and horizontal gaps + this.setVgap(20); + this.setHgap(20); + + // Get a picture of a microphone for the voice recording button + File file = new File(AppConfig.MICROPHONE_IMG_FILE); + microphone = new ImageView(new Image(file.toURI().toString())); + + // Set the size of the microphone image + microphone.setFitWidth(50); + microphone.setFitHeight(50); + microphone.setScaleX(1); + microphone.setScaleY(1); + + // Create a recording button + recordButton = new Button(); + recordButton.setGraphic(microphone); + + // Create a label that indicated if the app is currently recording + recordingLabel = new Label("Recording..."); + recordingLabel.setTextFill(Color.web("#FF0000")); + recordingLabel.setVisible(false); + + // Set the user prompt for meal type selection + prompt = new Label("Select Meal Type (Breakfast, Lunch, or Dinner)"); + prompt.setStyle("-fx-font-size: 16; -fx-font-weight: bold;"); + // prompt.setTextFill(Color.web("#FF0000")); + + // Set a textField for the meal type that was selected + mealTypeArea = new TextArea(); + mealTypeArea.setPromptText("Meal Type"); + mealTypeArea.setStyle("-fx-font-size: 16"); // CHANGE 1 (FONT) + mealTypeArea.setPrefWidth(300); + mealTypeArea.setPrefHeight(50); + mealTypeArea.setEditable(true); + + // Add all of the elements to the MealTypeSelection + this.add(recordButton, 0, 0); + this.add(recordingLabel, 0, 1); + this.add(prompt, 0, 2); + this.add(mealTypeArea, 0, 3); + + } + + public TextArea getMealType() { + return mealTypeArea; + } + + public Button getRecordButton() { + return recordButton; + } + + public Label getRecordingLabel() { + return recordingLabel; + } +} \ No newline at end of file diff --git a/app/src/main/java/code/client/View/RecipeUI.java b/app/src/main/java/code/client/View/RecipeUI.java index 983792c..d923a3c 100644 --- a/app/src/main/java/code/client/View/RecipeUI.java +++ b/app/src/main/java/code/client/View/RecipeUI.java @@ -37,34 +37,11 @@ public class RecipeUI extends HBox { marginMaker.setDisable(true); style.getChildren().addAll(marginMaker, mealType, detailsButton, deleteButton); style.setStyle("-fx-background-color: #DAE5EA; -fx-border-width: 0;"); - styleTags(mealType); - // + MealTagStyler.styleTags(recipe, mealType); this.getChildren().add(style); this.setPrefSize(50, 50); } - private void styleTags(Button mealType) { - switch (recipe.getMealTag().toLowerCase()) { - case "breakfast": - mealType.setStyle( - "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #FF7276; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); - mealType.setText("Breakfast"); - break; - - case "lunch": - mealType.setStyle( - "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FFFF; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); - mealType.setText("Lunch"); - break; - - case "dinner": - mealType.setStyle( - "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FF00; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); - mealType.setText("Dinner"); - break; - } - } - public Recipe getRecipe() { return this.recipe; } diff --git a/app/src/main/java/code/server/AppServer.java b/app/src/main/java/code/server/AppServer.java index 850c059..8c5ef73 100644 --- a/app/src/main/java/code/server/AppServer.java +++ b/app/src/main/java/code/server/AppServer.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.net.URISyntaxException; import java.util.concurrent.*; import org.bson.Document; @@ -44,6 +45,13 @@ public void start() throws IOException { httpServer.createContext(AppConfig.RECIPE_PATH, new RecipeRequestHandler(recipeDb)); httpServer.createContext(AppConfig.ACCOUNT_PATH, new AccountRequestHandler(accountMongoDB)); httpServer.createContext(AppConfig.SHARE_PATH, new ShareRequestHandler(accountMongoDB, recipeDb)); + httpServer.createContext(AppConfig.CHATGPT_PATH, new ChatGPTRequestHandler()); + httpServer.createContext(AppConfig.DALLE_PATH, new DallERequestHandler()); + try { + httpServer.createContext(AppConfig.WHISPER_PATH, new WhisperRequestHandler()); + } catch (URISyntaxException e) { + e.printStackTrace(); + } // set the executor httpServer.setExecutor(threadPoolExecutor); // start the server diff --git a/app/src/main/java/code/server/ChatGPTRequestHandler.java b/app/src/main/java/code/server/ChatGPTRequestHandler.java new file mode 100644 index 0000000..3c37758 --- /dev/null +++ b/app/src/main/java/code/server/ChatGPTRequestHandler.java @@ -0,0 +1,87 @@ +package code.server; + +import code.client.Model.AppConfig; +import com.sun.net.httpserver.*; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.json.JSONArray; +import org.json.JSONObject; + +public class ChatGPTRequestHandler extends TextToRecipe implements HttpHandler { + private static final String API_ENDPOINT = "https://api.openai.com/v1/completions"; + private static final String MODEL = "text-davinci-003"; + private static final int MAX_TOKENS = 500; + private static final double TEMPERATURE = 1.; + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + String response = "Request received"; + URI uri = httpExchange.getRequestURI(); + String query = uri.getRawQuery(); + + try { + String value = query.substring(query.indexOf("=") + 1); + String[] typeIngredients = value.split("::"); + String mealType = typeIngredients[0]; + String ingredients = typeIngredients[1]; + response = getResponse(mealType, ingredients); + } catch (IndexOutOfBoundsException e) { + response = "Provide valid meal type or ingredients"; + e.printStackTrace(); + } catch (InterruptedException | URISyntaxException e) { + response = "An error occurred."; + e.printStackTrace(); + } + + // Sending back response to the client + httpExchange.sendResponseHeaders(200, response.length()); + OutputStream outStream = httpExchange.getResponseBody(); + outStream.write(response.getBytes()); + outStream.close(); + } + + private String getResponse(String mealType, String ingredients) + throws IOException, InterruptedException, URISyntaxException { + // Set request parameters + String prompt = buildPrompt(mealType, ingredients); + + // Create a request body which you will pass into request object + JSONObject requestBody = new JSONObject(); + requestBody.put("model", MODEL); + requestBody.put("prompt", prompt); + requestBody.put("max_tokens", MAX_TOKENS); + requestBody.put("temperature", TEMPERATURE); + + // Create the HTTP Client + HttpClient client = HttpClient.newHttpClient(); + + // Create the request object + HttpRequest request = HttpRequest + .newBuilder() + .uri(URI.create(API_ENDPOINT)) + .header("Content-Type", "application/json") + .header("Authorization", String.format("Bearer %s", AppConfig.API_KEY)) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) + .build(); + + HttpResponse response = client.send( + request, + HttpResponse.BodyHandlers.ofString()); + + // Process the response + String responseBody = response.body(); + JSONObject responseJson = new JSONObject(responseBody); + JSONArray choices = responseJson.getJSONArray("choices"); + String responseText = choices.getJSONObject(0).getString("text"); + return responseText; + } + + @Override + public void setSampleRecipe(String recipe) { + } +} \ No newline at end of file diff --git a/app/src/main/java/code/server/DallERequestHandler.java b/app/src/main/java/code/server/DallERequestHandler.java new file mode 100644 index 0000000..203249d --- /dev/null +++ b/app/src/main/java/code/server/DallERequestHandler.java @@ -0,0 +1,99 @@ +package code.server; + +import code.client.Model.AppConfig; +import com.sun.net.httpserver.*; + +import java.io.*; +import java.net.*; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; + +import org.json.JSONObject; +import java.util.Base64; + +public class DallERequestHandler extends RecipeToImage implements HttpHandler { + private static final String API_ENDPOINT = "https://api.openai.com/v1/images/generations"; + private static final String MODEL = "dall-e-2"; + private static final int NUM_IMAGES = 1; + private static final String IMAGE_SIZE = "256x256"; + private static final String RESPONSE_FORMAT = "b64_json"; + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + String response = "Request received"; + URI uri = httpExchange.getRequestURI(); + String query = uri.getRawQuery(); + try { + String recipeTitle = query.substring(query.indexOf("=") + 1); + response = getResponse(recipeTitle); + } catch (InterruptedException e) { + response = "An error occurred."; + e.printStackTrace(); + } + + // Sending back response to the client + httpExchange.sendResponseHeaders(200, response.length()); + OutputStream outStream = httpExchange.getResponseBody(); + outStream.write(response.getBytes()); + outStream.close(); + } + + private String getResponse(String prompt) throws IOException, InterruptedException { + // Create a request body which you will pass into request object + JSONObject requestBody = new JSONObject(); + requestBody.put("model", MODEL); + requestBody.put("prompt", prompt); + requestBody.put("n", NUM_IMAGES); + requestBody.put("size", IMAGE_SIZE); + requestBody.put("response_format", RESPONSE_FORMAT); + + // Create the HTTP client + HttpClient client = HttpClient.newHttpClient(); + + // Create the request object + HttpRequest request = HttpRequest + .newBuilder() + .uri(URI.create(API_ENDPOINT)) + .header("Content-Type", "application/json") + .header("Authorization", String.format("Bearer %s", AppConfig.API_KEY)) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) + .build(); + + // Send the request and receive the response + HttpResponse response = client.send( + request, + HttpResponse.BodyHandlers.ofString()); + + // Process the response + String responseBody = response.body(); + return processResponse(responseBody); + } + + private String processResponse(String responseBody) { + JSONObject responseJson = new JSONObject(responseBody); + String generatedImageData = ""; + try { + generatedImageData = responseJson.getJSONArray("data") + .getJSONObject(0).getString("b64_json"); + } catch (Exception chatError) { + // badly formatted json + File file = new File(AppConfig.RECIPE_IMG_FILE); + try { + byte[] imageBytes = Files.readAllBytes(file.toPath()); + generatedImageData = Base64.getEncoder().encodeToString(imageBytes); + } catch (Exception fileError) { + fileError.printStackTrace(); + } + chatError.printStackTrace(); + } + return generatedImageData; + } + + @Override + public void setError(boolean error) { + } +} \ No newline at end of file diff --git a/app/src/main/java/code/server/MockChatGPTRequestHandler.java b/app/src/main/java/code/server/MockChatGPTRequestHandler.java new file mode 100644 index 0000000..8ded3e2 --- /dev/null +++ b/app/src/main/java/code/server/MockChatGPTRequestHandler.java @@ -0,0 +1,34 @@ +package code.server; + +import java.io.IOException; +import java.io.OutputStream; +import com.sun.net.httpserver.*; + +public class MockChatGPTRequestHandler extends TextToRecipe implements HttpHandler { + + private String sampleRecipe = """ + Fried Chicken + breakfast + Ingredients: + - 2 chicken breasts, diced + - 2 eggs + Instructions: + 1. Crack 2 eggs into bowl. + 2. Add chicken into bowl and then fry. + 3. Enjoy! + """; + + @Override + public void setSampleRecipe(String recipeText) { + sampleRecipe = recipeText; + } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + httpExchange.sendResponseHeaders(200, sampleRecipe.length()); + OutputStream outStream = httpExchange.getResponseBody(); + outStream.write(sampleRecipe.getBytes()); + outStream.close(); + + } +} diff --git a/app/src/main/java/code/server/MockDallERequestHandler.java b/app/src/main/java/code/server/MockDallERequestHandler.java new file mode 100644 index 0000000..6db18cf --- /dev/null +++ b/app/src/main/java/code/server/MockDallERequestHandler.java @@ -0,0 +1,70 @@ +package code.server; + +import code.client.Model.AppConfig; +import com.sun.net.httpserver.*; + +import java.io.*; +import java.net.*; + +import java.io.IOException; +import java.nio.file.Files; + +import java.util.Base64; + +public class MockDallERequestHandler extends RecipeToImage implements HttpHandler { + private boolean error = false; + + @Override + public void setError(boolean error) { + this.error = error; + } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + String response = "Request received"; + String method = httpExchange.getRequestMethod(); + System.out.println("Method is " + method); + + try { + if (method.equals("GET")) { + response = handleGet(httpExchange); + } else { + throw new Exception("Not valid request method."); + } + } catch (Exception e) { + System.out.println("An erroneous request"); + e.printStackTrace(); + } + + // Sending back response to the client + httpExchange.sendResponseHeaders(200, response.length()); + OutputStream outStream = httpExchange.getResponseBody(); + outStream.write(response.getBytes()); + outStream.close(); + } + + private String handleGet(HttpExchange httpExchange) throws IOException, InterruptedException { + String response = "Invalid GET request"; + URI uri = httpExchange.getRequestURI(); + String query = uri.getRawQuery(); + + if (query != null && !error) { + String recipeTitle = query.substring(query.indexOf("=") + 1); + response = getResponse(recipeTitle); + } + return response; + } + + private String getResponse(String recipeText) throws IOException, InterruptedException { + File file = new File(AppConfig.RECIPE_IMG_FILE); + // try to give default image + try { + byte[] imageBytes = Files.readAllBytes(file.toPath()); + return Base64.getEncoder().encodeToString(imageBytes); + } catch (Exception fileError) { + fileError.printStackTrace(); + return "RnJpZWQgUmljZSBJbWFnZSA6KQ=="; + } + } + +} diff --git a/app/src/main/java/code/server/MockWhisperRequestHandler.java b/app/src/main/java/code/server/MockWhisperRequestHandler.java new file mode 100644 index 0000000..47a4b79 --- /dev/null +++ b/app/src/main/java/code/server/MockWhisperRequestHandler.java @@ -0,0 +1,28 @@ +package code.server; + +import java.io.IOException; +import java.net.URISyntaxException; + +import code.client.Model.IHttpConnection; +import code.client.Model.MockHttpConnection; + +public class MockWhisperRequestHandler extends VoiceToText { + public MockWhisperRequestHandler() { + super(new MockHttpConnection(200)); + } + + public MockWhisperRequestHandler(IHttpConnection connection) { + super(connection); + } + + public String processAudio(String type) throws IOException, URISyntaxException { + if (type.equals("mealtype")) { + // processed correctly + return "Breakfast"; + } else if (type.equals("ingredients")) { + return "Chicken, eggs."; + } else { + return "Error text"; + } + } +} diff --git a/app/src/main/java/code/server/RecipeBuilder.java b/app/src/main/java/code/server/RecipeBuilder.java index 5bd0e1a..a55bec9 100644 --- a/app/src/main/java/code/server/RecipeBuilder.java +++ b/app/src/main/java/code/server/RecipeBuilder.java @@ -13,11 +13,11 @@ public class RecipeBuilder { public RecipeBuilder(String userID, String title) { recipe = new Recipe(new ObjectId().toHexString(), - userID, - title, - "", - 0, - getDefaultImage()); + userID, + title, + "breakfast", + 0, + getDefaultImage()); } public RecipeBuilder setId(String id) { @@ -53,14 +53,14 @@ public RecipeBuilder setImage(String image) { private String getDefaultImage() { String image = ""; File file = new File(AppConfig.RECIPE_IMG_FILE); - + try { byte[] imageBytes = Files.readAllBytes(file.toPath()); image = Base64.getEncoder().encodeToString(imageBytes); } catch (Exception fileError) { fileError.printStackTrace(); } - + return image; } diff --git a/app/src/main/java/code/server/RecipeRequestHandler.java b/app/src/main/java/code/server/RecipeRequestHandler.java index fd9fcc4..0e3f3c1 100644 --- a/app/src/main/java/code/server/RecipeRequestHandler.java +++ b/app/src/main/java/code/server/RecipeRequestHandler.java @@ -43,38 +43,6 @@ public void handle(HttpExchange httpExchange) throws IOException { outStream.close(); } - private String buildResponseFromRecipe(Recipe recipe) { - return recipe.toString(); - } - - private String getRecipeById(String id) { - String response; - Recipe recipe = recipeDb.find(id); - - if (recipe != null) { - response = buildResponseFromRecipe(recipe); - System.out.println("Queried for " + id + " and found " + recipe.getTitle()); - } else { - response = "Recipe not found."; - } - - return response; - } - - private String getAllRecipes() { - List recipeList = recipeDb.getList(); - StringBuilder responseBuilder = new StringBuilder(); - String recipeResponse; - - for (Recipe recipe : recipeList) { - recipeResponse = buildResponseFromRecipe(recipe); - responseBuilder.append(recipeResponse).append("\n"); - } - - String response = responseBuilder.toString(); - return response; - } - private String handleGet(HttpExchange httpExchange) throws IOException { String response = "Invalid GET request"; URI uri = httpExchange.getRequestURI(); diff --git a/app/src/main/java/code/server/RecipeToImage.java b/app/src/main/java/code/server/RecipeToImage.java new file mode 100644 index 0000000..9004124 --- /dev/null +++ b/app/src/main/java/code/server/RecipeToImage.java @@ -0,0 +1,11 @@ +package code.server; + +import java.io.IOException; + +import com.sun.net.httpserver.HttpExchange; + +public abstract class RecipeToImage { + public abstract void handle(HttpExchange httpExchange) throws IOException; + + public abstract void setError(boolean error); +} diff --git a/app/src/main/java/code/server/ShareRecipe.java b/app/src/main/java/code/server/ShareRecipe.java new file mode 100644 index 0000000..2a7e10c --- /dev/null +++ b/app/src/main/java/code/server/ShareRecipe.java @@ -0,0 +1,101 @@ +package code.server; + +import java.util.Iterator; +import java.util.List; + +public class ShareRecipe { + public static String getSharedRecipe(AccountMongoDB accountMongoDB, IRecipeDb recipeMongoDb, String username, + String recipeID) { + Account checkUser = accountMongoDB.findByUsername(username); + + if (checkUser == null) { + return nonExistentRecipe(); + } + // System.out.println("Found user" + checkUser.getUsername()); + String accountID = checkUser.getId(); + List accRecipes = recipeMongoDb.getList(accountID); + Recipe foundRecipe = null; + for (int i = 0; i < accRecipes.size(); i++) { + if (accRecipes.get(i).getId().equals(recipeID)) { + foundRecipe = accRecipes.get(i); + } + } + + if (foundRecipe == null) { + return nonExistentRecipe(); + } + System.out.println("Found recipe" + foundRecipe.getTitle()); + return formatRecipe(foundRecipe); + } + + private static String nonExistentRecipe() { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder + .append("") + .append("") + .append("

") + .append("The provided link is not associated to a recipe.") + .append("

") + .append("") + .append(""); + // encode HTML content + return htmlBuilder.toString(); + } + + private static String getMockedRecipe() { + String title = "Fried Chicken and Egg Fried Rice"; + String mealType = "Breakfast"; + String ingredients = "2 chicken breasts, diced;;2 large eggs;;2 cups cooked rice;;2 tablespoons vegetable oil"; + String[] ingr = ingredients.split(";;"); + String instructions = "1. Heat the vegetable oil in a large pan over medium-high heat."; + String[] instr = instructions.split(";;"); + // return formatRecipe(title, ingr, instr); + return title; + } + + private static String formatRecipe(Recipe recipe) { + Iterator ingr = recipe.getIngredientIterator(); + Iterator instr = recipe.getInstructionIterator(); + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder + .append("") + .append("" + recipe.getTitle() + "") + .append("") + .append("

" + recipe.getTitle() + "

") + .append("") + .append("

") + .append("Ingredients: ") + .append("

") + .append("

") + .append("

    "); + while (ingr.hasNext()) { + htmlBuilder.append("
  • " + ingr.next() + "
  • "); + } + htmlBuilder + .append("
") + .append("

") + .append("") + .append("

") + .append("Instructions: ") + .append("

") + .append("

") + .append("

    "); + while (instr.hasNext()) { + htmlBuilder.append("
  • " + instr.next() + "
  • "); + } + htmlBuilder + .append("
") + .append("

") + .append(""); + + htmlBuilder + .append("") + .append(""); + + // encode HTML content + return htmlBuilder.toString(); + } + +} diff --git a/app/src/main/java/code/server/ShareRequestHandler.java b/app/src/main/java/code/server/ShareRequestHandler.java index 5fc29b2..a52bd27 100644 --- a/app/src/main/java/code/server/ShareRequestHandler.java +++ b/app/src/main/java/code/server/ShareRequestHandler.java @@ -39,7 +39,7 @@ public void handle(HttpExchange httpExchange) throws IOException { System.out.println("\n" + uri.toString()); System.out.println(username); System.out.println(recipeID); - response = getSharedRecipe(username, recipeID); + response = ShareRecipe.getSharedRecipe(accountMongoDB, recipeMongoDb, username, recipeID); // Sending back response to the client httpExchange.sendResponseHeaders(200, response.length()); OutputStream outStream = httpExchange.getResponseBody(); @@ -47,98 +47,4 @@ public void handle(HttpExchange httpExchange) throws IOException { outStream.close(); } - private String getSharedRecipe(String username, String recipeID) { - Account checkUser = accountMongoDB.findByUsername(username); - - if (checkUser == null) { - return nonExistentRecipe(); - } - // System.out.println("Found user" + checkUser.getUsername()); - String accountID = checkUser.getId(); - List accRecipes = recipeMongoDb.getList(accountID); - Recipe foundRecipe = null; - for (int i = 0; i < accRecipes.size(); i++) { - if (accRecipes.get(i).getId().equals(recipeID)) { - foundRecipe = accRecipes.get(i); - } - } - - if (foundRecipe == null) { - return nonExistentRecipe(); - } - System.out.println("Found recipe" + foundRecipe.getTitle()); - return formatRecipe(foundRecipe); - } - - private String nonExistentRecipe() { - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder - .append("") - .append("") - .append("

") - .append("The provided link is not associated to a recipe.") - .append("

") - .append("") - .append(""); - // encode HTML content - return htmlBuilder.toString(); - } - - private String getMockedRecipe() { - String title = "Fried Chicken and Egg Fried Rice"; - String mealType = "Breakfast"; - String ingredients = "2 chicken breasts, diced;;2 large eggs;;2 cups cooked rice;;2 tablespoons vegetable oil"; - String[] ingr = ingredients.split(";;"); - String instructions = "1. Heat the vegetable oil in a large pan over medium-high heat."; - String[] instr = instructions.split(";;"); - // return formatRecipe(title, ingr, instr); - return title; - } - - private String formatRecipe(Recipe recipe) { - Iterator ingr = recipe.getIngredientIterator(); - Iterator instr = recipe.getInstructionIterator(); - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder - .append("") - .append("" + recipe.getTitle() + "") - .append("") - - .append("

" + recipe.getTitle() + "

") - .append("") - .append("

") - .append("Ingredients: ") - .append("

") - .append("

") - .append("

    "); - while (ingr.hasNext()) { - htmlBuilder.append("
  • " + ingr.next() + "
  • "); - } - htmlBuilder - .append("
") - .append("

") - .append("") - .append("

") - .append("Instructions: ") - .append("

") - .append("

") - .append("

    "); - while (instr.hasNext()) { - htmlBuilder.append("
  • " + instr.next() + "
  • "); - } - htmlBuilder - .append("
") - .append("

") - .append(""); - - htmlBuilder - .append("") - .append(""); - - // encode HTML content - return htmlBuilder.toString(); - } - } diff --git a/app/src/main/java/code/server/TextToRecipe.java b/app/src/main/java/code/server/TextToRecipe.java new file mode 100644 index 0000000..c2cc29b --- /dev/null +++ b/app/src/main/java/code/server/TextToRecipe.java @@ -0,0 +1,24 @@ +package code.server; + +import com.sun.net.httpserver.*; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +public abstract class TextToRecipe { + public abstract void handle(HttpExchange httpExchange) throws IOException; + + public String buildPrompt(String mealType, String ingredients) { + StringBuilder prompt = new StringBuilder(); + prompt.append("I am a student on a budget with a busy schedule and I need to quickly cook a ") + .append(mealType) + .append(". ") + .append(ingredients) + .append(" Make a recipe using only these ingredients plus condiments. ") + .append("Remember to first include a title, then a list of ingredients, and then a list of instructions."); + return prompt.toString(); + } + + public abstract void setSampleRecipe(String recipe); +} diff --git a/app/src/main/java/code/server/VoiceToText.java b/app/src/main/java/code/server/VoiceToText.java new file mode 100644 index 0000000..36bffdb --- /dev/null +++ b/app/src/main/java/code/server/VoiceToText.java @@ -0,0 +1,80 @@ +package code.server; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; + +import org.json.JSONException; +import org.json.JSONObject; +import code.client.Model.IHttpConnection; + +public abstract class VoiceToText { + protected final IHttpConnection connection; + + public VoiceToText(IHttpConnection connection) { + this.connection = connection; + } + + public String processAudio(String type) throws IOException, URISyntaxException { + String response = handleResponse(); + if (type.equals("mealtype")) { + response = response.toUpperCase(); + if (response.contains("BREAKFAST")) { + response = "Breakfast"; + } else if (response.contains("LUNCH")) { + response = "Lunch"; + } else if (response.contains("DINNER")) { + response = "Dinner"; + } else { + response = null; + } + } + return response; + } + + private String handleResponse() throws IOException { + // Get response code + int responseCode = connection.getResponseCode(); + String response; + + // Check response code and handle response accordingly + if (responseCode == HttpURLConnection.HTTP_OK) { + response = handleSuccessResponse(); + } else { + response = handleErrorResponse(); + } + + return response; + } + + // Helper method to handle a successful response + private String handleSuccessResponse() throws IOException, JSONException { + BufferedReader responseReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + StringBuilder response = new StringBuilder(); + + while ((inputLine = responseReader.readLine()) != null) { + response.append(inputLine); + } + + responseReader.close(); + JSONObject responseJson = new JSONObject(response.toString()); + String generatedText = responseJson.getString("text"); + return generatedText; + } + + // Helper method to handle an error response + private String handleErrorResponse() throws IOException, JSONException { + BufferedReader errorReader = new BufferedReader(new InputStreamReader(connection.getErrorStream())); + String errorLine; + StringBuilder errorResponse = new StringBuilder(); + + while ((errorLine = errorReader.readLine()) != null) { + errorResponse.append(errorLine); + } + + errorReader.close(); + String errorResult = errorResponse.toString(); + return errorResult; + } +} diff --git a/app/src/main/java/code/server/WHISPERSERVER.java b/app/src/main/java/code/server/WHISPERSERVER.java new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/code/server/WhisperRequestHandler.java b/app/src/main/java/code/server/WhisperRequestHandler.java new file mode 100644 index 0000000..c3a7e7b --- /dev/null +++ b/app/src/main/java/code/server/WhisperRequestHandler.java @@ -0,0 +1,142 @@ +package code.server; + +import com.sun.net.httpserver.*; + +import code.client.Model.AppConfig; +import code.client.Model.AppHttpConnection; +import java.io.*; +import java.net.*; + +public class WhisperRequestHandler extends VoiceToText implements HttpHandler { + public static final String API_ENDPOINT = "https://api.openai.com/v1/audio/transcriptions"; + public static final String MODEL = "whisper-1"; + + public WhisperRequestHandler() throws URISyntaxException, IOException { + super(new AppHttpConnection(API_ENDPOINT)); + } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + String response = "Request received"; + String method = httpExchange.getRequestMethod(); + System.out.println("Method is " + method); + int audioFileSize = 0; + + try { + File audioFile = new File(AppConfig.AUDIO_FILE); + + if (!audioFile.exists()) { + audioFile.createNewFile(); + } + + InputStream multipartInStream = httpExchange.getRequestBody(); + String nextLine = ""; + + do { + nextLine = readLine(multipartInStream, "\r\n"); + if (nextLine.startsWith("Content-Length:")) { + audioFileSize = Integer.parseInt( + nextLine.replaceAll(" ", "").substring( + "Content-Length:".length())); + } + } while (!nextLine.equals("")); + + byte[] audioByteArray = new byte[audioFileSize]; + int readOffset = 0; + + while (readOffset < audioFileSize) { + int bytesRead = multipartInStream.read(audioByteArray, readOffset, audioFileSize); + readOffset += bytesRead; + } + + BufferedOutputStream audioOutStream = new BufferedOutputStream(new FileOutputStream(AppConfig.AUDIO_FILE)); + audioOutStream.write(audioByteArray, 0, audioFileSize); + audioOutStream.flush(); + audioOutStream.close(); + } catch (Exception e) { + System.out.println("An erroneous request"); + e.printStackTrace(); + } + + // Sending back response to the client + httpExchange.sendResponseHeaders(200, response.length()); + OutputStream outStream = httpExchange.getResponseBody(); + outStream.write(response.getBytes()); + outStream.close(); + } + + private static String readLine(InputStream multipartInStream, String lineSeparator) throws IOException { + int offset = 0, i = 0; + byte[] separator = lineSeparator.getBytes("UTF-8"); + byte[] lineBytes = new byte[1024]; + + while (multipartInStream.available() > 0) { + int nextByte = multipartInStream.read(); + if (nextByte < -1) { + throw new IOException("Reached end of stream while reading the current line!"); + } + + lineBytes[i] = (byte) nextByte; + + if (lineBytes[i++] == separator[offset++]) { + if (offset == separator.length) { + return new String(lineBytes, 0, i - separator.length, "UTF-8"); + } + } else { + offset = 0; + } + + if (i == lineBytes.length) { + throw new IOException("Maximum line length exceeded: " + i); + } + } + + throw new IOException("Reached end of stream while reading the current line!"); + } + + // https://stackoverflow.com/questions/25334139/how-to-mock-a-url-connection + public String processAudio(String type) throws IOException, URISyntaxException { + // Send HTTP request + sendHttpRequest(); + return super.processAudio(type); + } + + private void sendHttpRequest() throws IOException, URISyntaxException { + // Set up request headers + File file = new File(AppConfig.AUDIO_FILE); + String boundary = "Boundary-" + System.currentTimeMillis(); + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + connection.setRequestProperty("Authorization", "Bearer " + AppConfig.API_KEY); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + + // Set up output stream to write request body + OutputStream outputStream = connection.getOutputStream(); + // Write model parameter to request body + outputStream.write(("--" + boundary + "\r\n").getBytes()); + outputStream.write( + ("Content-Disposition: form-data; name=\"model\"\r\n\r\n").getBytes()); + outputStream.write((MODEL + "\r\n").getBytes()); + // Write file parameter to request body + outputStream.write(("--" + boundary + "\r\n").getBytes()); + outputStream.write(("Content-Disposition: form-data; name=\"file\"; filename=\"" + + file.getName() + + "\"\r\n").getBytes()); + outputStream.write(("Content-Type: audio/mpeg\r\n\r\n").getBytes()); + + FileInputStream fileInputStream = new FileInputStream(file); + byte[] buffer = new byte[1024]; + int bytesRead; + + while ((bytesRead = fileInputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + + fileInputStream.close(); + // Write closing boundary to request body + outputStream.write(("\r\n--" + boundary + "--\r\n").getBytes()); + // Flush and close output stream + outputStream.flush(); + outputStream.close(); + } +} \ No newline at end of file diff --git a/app/src/test/java/code/AudioRecorderTest.java b/app/src/test/java/code/AudioRecorderTest.java new file mode 100644 index 0000000..dc7aa7f --- /dev/null +++ b/app/src/test/java/code/AudioRecorderTest.java @@ -0,0 +1,26 @@ +package code; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import code.server.*; +import code.client.Model.*; +import static org.junit.jupiter.api.Assertions.*; +import java.util.*; + +import java.io.*; +import java.net.URISyntaxException; + +public class AudioRecorderTest { + private BaseAudioRecorder audioRecorder; + + @Test + public void testRecording() { + Writer writer = new StringWriter(); + audioRecorder = new MockAudioRecorder(writer); + audioRecorder.startRecording(); + audioRecorder.stopRecording(); + String expected = "Recipe recorded."; + assertEquals(expected, writer.toString()); + } +} diff --git a/app/src/test/java/code/AutologinTest.java b/app/src/test/java/code/AutologinTest.java index 2aee2a0..9d4c7e6 100644 --- a/app/src/test/java/code/AutologinTest.java +++ b/app/src/test/java/code/AutologinTest.java @@ -7,31 +7,55 @@ import code.client.Model.Account; import code.client.Model.AccountCSVWriter; +import code.client.Model.AccountCSVReader; import java.io.IOException; -import java.io.StringWriter; +import java.io.FileWriter; +import java.io.FileReader; +import java.util.List; +import java.util.ArrayList; + +/** + * Test file for testing the automatic login feature. Specifically tests whether + * or + * not a username and password are correctly written to a UserCredentials.csv + * file + */ public class AutologinTest { - private Account account; + private Account account; // Account used for the test + private AccountCSVWriter writer; // Writer that saves user credentials to a csv file + private AccountCSVReader reader; // Reader that reads user credentials from a csv file + private List userCredentials; // Helper variable that stores credentials read from the csv file /** - * Before running the test, set up an account for automatic log in + * Before running the test, set up an account with a username and password. Also + * initialize the userCredentials to an empty list */ @BeforeEach public void setUp() throws IOException { - account = new Account("GMIRANDA", "CSE110"); + account = new Account("Chef", "Caitlyn"); + userCredentials = new ArrayList<>(); } /** - * Save user credentials to a file called userCredentials.csv - * Expected result: A userCredentials.csv file with the user credentials + * Save user credentials to a file called UserCredentialsTest.csv + * Expected result: A UserCredentialsTest.csv file with the user credentials */ @Test public void testSaveUserCredentials() throws IOException { - StringWriter writer = new StringWriter(); - AccountCSVWriter accountWriter = new AccountCSVWriter(writer); - accountWriter.writeAccount(account.getUsername(), account.getPassword()); - String expected = "GMIRANDA|CSE110"; - assertEquals(expected, writer.toString()); + // Save the username and password to a file called "UserCredentialsTest.csv" + writer = new AccountCSVWriter(new FileWriter("UserCredentialsTest.csv")); + writer.writeAccount(account.getUsername(), account.getPassword()); + writer.close(); + // Read the username and password from a file called "UserCredentialsTest.csv" + reader = new AccountCSVReader(new FileReader("UserCredentialsTest.csv")); + userCredentials = reader.readUserCredentials(); + reader.close(); + // Check that the username and password were correctly saved to the csv file + String expectedUsername = "Chef"; + String expectedPassword = "Caitlyn"; + assertEquals(expectedUsername, userCredentials.get(0)); + assertEquals(expectedPassword, userCredentials.get(1)); } } \ No newline at end of file diff --git a/app/src/test/java/code/EndToEndScenario2_1.java b/app/src/test/java/code/EndToEndScenario2_1.java new file mode 100644 index 0000000..2f9ad2c --- /dev/null +++ b/app/src/test/java/code/EndToEndScenario2_1.java @@ -0,0 +1,120 @@ +package code; + +import org.junit.jupiter.api.Test; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; + +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.FileWriter; +import java.io.FileReader; +import java.io.IOException; +import java.util.List; + +import code.server.Account; +import code.client.Model.AccountCSVReader; +import code.client.Model.AccountCSVWriter; +import code.client.Model.AppConfig; +import code.client.View.*; +import code.client.Controllers.*; +import code.server.*; + +/** + * This test file covers the End-to-End Scenario in which Chef Caitlyn: + * 1. Creates an account + * 2. Opts for automatic login + * 3. Creates a new recipe + * 4. Refreshes the new recipe + * 5. Saves the refreshed recipe + */ +public class EndToEndScenario2_1 { + private Account account; // Account used in the following tests + + @BeforeEach + public void setUp() { + account = new Account("Chef", "Caitlyn"); + } + + /** + * Test that Chef Caitlyn was able to successfully create an account on MongoDB + */ + @Test + public void createAccountTest() { + try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { + MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); + MongoCollection accountCollection = mongoDb.getCollection(AppConfig.MONGO_USER_COLLECTION); + AccountMongoDB accountDb = new AccountMongoDB(accountCollection); + accountDb.add(account); + assertTrue(accountDb.findByUsername("Chef") != null); + accountDb.removeByUsername("Chef"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Test that Chef Caitlyn's user credentials were successfully saved onto her + * device + */ + @Test + public void automaticLoginTest() throws IOException { + // Save the username and password to a file called "UserCredentialsTest.csv" + AccountCSVWriter writer = new AccountCSVWriter(new FileWriter("UserCredentialsTest.csv")); + writer.writeAccount(account.getUsername(), account.getPassword()); + writer.close(); + // Read the username and password from a file called "UserCredentialsTest.csv" + AccountCSVReader reader = new AccountCSVReader(new FileReader("UserCredentialsTest.csv")); + List userCredentials = reader.readUserCredentials(); + reader.close(); + // Check that the username and password were correctly saved to the csv file + String expectedUsername = "Chef"; + String expectedPassword = "Caitlyn"; + assertEquals(expectedUsername, userCredentials.get(0)); + assertEquals(expectedPassword, userCredentials.get(1)); + } + + /** + * Test that Chef Caitlyn can successfully create a recipe + */ + @Test + public void createRecipeTest() { + + } + + /** + * Test that Chef Caitlyn can successfully regenerate a recipe if she doesn't + * like it. + */ + @Test + public void refreshRecipeTest() { + + } + + /** + * Test that Chef Caitlyn can successfully save a recipe + */ + @Test + public void saveRecipeTest() { + RecipeBuilder builder = new RecipeBuilder(account.getId(), "Caitlyn's Lunch"); + builder.setMealTag("lunch"); + Recipe recipe = builder.buildRecipe(); + try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { + MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); + MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); + RecipeMongoDb recipeDB = new RecipeMongoDb(recipeCollection); + recipeDB.add(recipe); + Recipe receivedRecipe = recipeDB.find(recipe.getId()); + assertTrue(receivedRecipe != null); + assertTrue(receivedRecipe.getAccountId().contains(account.getId())); + recipeDB.remove(recipe.getId()); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/app/src/test/java/code/EndToEndScenario2_2.java b/app/src/test/java/code/EndToEndScenario2_2.java new file mode 100644 index 0000000..022b776 --- /dev/null +++ b/app/src/test/java/code/EndToEndScenario2_2.java @@ -0,0 +1,39 @@ +package code; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.FileWriter; +import java.io.FileReader; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +import code.client.Model.*; +import code.client.View.*; +import code.client.Controllers.*; +import code.server.*; + +public class EndToEndScenario2_2 { + @Test + public void serverUnavailableTest() { + } + + @Test + public void loginSuccessfulTest() { + } + + @Test + public void sortRecipeListTest() { + } + + @Test + public void filterRecipeListTest() { + } + + @Test + public void shareRecipeListTest() { + + } +} \ No newline at end of file diff --git a/app/src/test/java/code/RefreshRecipeTest.java b/app/src/test/java/code/RefreshRecipeTest.java deleted file mode 100644 index cc03cc0..0000000 --- a/app/src/test/java/code/RefreshRecipeTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package code; - -public class RefreshRecipeTest { - -} diff --git a/app/src/test/java/code/RefreshTest.java b/app/src/test/java/code/RefreshTest.java new file mode 100644 index 0000000..4e63ef0 --- /dev/null +++ b/app/src/test/java/code/RefreshTest.java @@ -0,0 +1,47 @@ +package code; + +import code.client.Controllers.Controller; +import code.client.Model.MockGPTService; +import code.client.Model.MockDallEService; +import code.client.Model.Model; +import code.client.View.DetailsAppFrame; +import code.client.View.View; +import javafx.scene.control.Button; +import javafx.stage.Stage; +import org.junit.jupiter.api.Test; +import code.client.Model.Account; +import code.client.Model.AccountCSVReader; +import code.client.Model.AccountCSVWriter; +import code.client.Model.ChatGPTService; +import code.client.View.*; +import code.client.Controllers.*; +import code.server.*; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.*; + +public class RefreshTest { + @Test + public void refreshTesting() { + DetailsAppFrame detailsAppFrame = new DetailsAppFrame(); + // TextToRecipe txt = new TextToRecipe(); + // initial recipe before refresh + Recipe initialRecipe = detailsAppFrame.getDisplayedRecipe(); + + MockGPTService mockGPT = new MockGPTService(); + Recipe refreshedRecipe; + } + + private Recipe generateRefresh(Recipe originalRecipe, MockGPTService mockGPTService) + throws IOException, InterruptedException, URISyntaxException { + String mealType = "Breakfast"; + String ingredients = "Chicken, eggs."; + + String refreshedResponse = mockGPTService.getResponse(mealType, ingredients); + Recipe out = mockGPTService.mapResponseToRecipe(mealType, refreshedResponse); + + return out; + } + +} \ No newline at end of file diff --git a/app/src/test/java/code/ShareRecipeTest.java b/app/src/test/java/code/ShareRecipeTest.java new file mode 100644 index 0000000..5fe9b7b --- /dev/null +++ b/app/src/test/java/code/ShareRecipeTest.java @@ -0,0 +1,69 @@ +package code; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.bson.Document; +import org.junit.jupiter.api.Test; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import static org.junit.jupiter.api.Assertions.*; +import code.client.Model.AppConfig; +import code.server.Account; +import code.server.AccountMongoDB; +import code.server.RecipeMongoDb; +import code.server.ShareRecipe; + +import java.net.HttpURLConnection; +import java.net.URL; + +public class ShareRecipeTest { + // RecipeID to check 656d2d5d0cde2c34c86b7651 from lol + @Test + public void testShareLink() { + try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { + MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); + MongoCollection accountCollection = mongoDb.getCollection(AppConfig.MONGO_USER_COLLECTION); + AccountMongoDB accountDb = new AccountMongoDB(accountCollection); + MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); + RecipeMongoDb recipeDb = new RecipeMongoDb(recipeCollection); + + String shareLink = ShareRecipe.getSharedRecipe(accountDb, recipeDb, "lol", "656d2d5d0cde2c34c86b7651"); + URL url = new URL(shareLink); + HttpURLConnection huc = (HttpURLConnection) url.openConnection(); + assertTrue(huc.getResponseMessage().length() > 50); + if (huc.getResponseCode() != 404) { + System.out.println("exists"); + } else { + System.out.println("does not exists"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testNonShareLink() { + try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { + MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); + MongoCollection accountCollection = mongoDb.getCollection(AppConfig.MONGO_USER_COLLECTION); + AccountMongoDB accountDb = new AccountMongoDB(accountCollection); + MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); + RecipeMongoDb recipeDb = new RecipeMongoDb(recipeCollection); + + String shareLink = ShareRecipe.getSharedRecipe(accountDb, recipeDb, "lol", "nonExistent"); + URL url = new URL(shareLink); + HttpURLConnection huc = (HttpURLConnection) url.openConnection(); + assertTrue(huc.getResponseMessage().length() < 50); + if (huc.getResponseCode() != 404) { + System.out.println("exists"); + } else { + System.out.println("does not exists"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file From 5c28b76e19bbe75f869273afd70463a1453f61a4 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Mon, 4 Dec 2023 18:01:50 -0800 Subject: [PATCH 05/31] refactor: dalle request handler --- .../java/code/client/Model/DallEService.java | 93 ------------------- .../code/client/Model/MockDallEService.java | 39 -------- .../main/java/code/client/Model/Model.java | 2 +- .../java/code/server/DallERequestHandler.java | 6 +- .../code/server/MockDallERequestHandler.java | 34 +------ app/src/main/java/code/server/MockServer.java | 36 +++++++ .../main/java/code/server/RecipeToImage.java | 2 - app/src/test/java/code/RecipeToImageTest.java | 46 +++------ 8 files changed, 55 insertions(+), 203 deletions(-) delete mode 100644 app/src/main/java/code/client/Model/DallEService.java delete mode 100644 app/src/main/java/code/client/Model/MockDallEService.java diff --git a/app/src/main/java/code/client/Model/DallEService.java b/app/src/main/java/code/client/Model/DallEService.java deleted file mode 100644 index 1542645..0000000 --- a/app/src/main/java/code/client/Model/DallEService.java +++ /dev/null @@ -1,93 +0,0 @@ -package code.client.Model; - -import java.io.IOException; -import java.io.InputStream; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.file.Files; -import java.nio.file.Paths; - -import org.json.JSONObject; -import java.util.Base64; - -public class DallEService extends RecipeToImage { - private static final String API_ENDPOINT = "https://api.openai.com/v1/images/generations"; - private static final String MODEL = "dall-e-2"; - private static final int NUM_IMAGES = 1; - private static final String IMAGE_SIZE = "256x256"; - private static final String RESPONSE_FORMAT = "b64_json"; - - @Override - public String getResponse(String prompt) throws IOException, InterruptedException { - // Create a request body which you will pass into request object - JSONObject requestBody = new JSONObject(); - requestBody.put("model", MODEL); - requestBody.put("prompt", prompt); - requestBody.put("n", NUM_IMAGES); - requestBody.put("size", IMAGE_SIZE); - requestBody.put("response_format", RESPONSE_FORMAT); - - // Create the HTTP client - HttpClient client = HttpClient.newHttpClient(); - - // Create the request object - HttpRequest request = HttpRequest - .newBuilder() - .uri(URI.create(API_ENDPOINT)) - .header("Content-Type", "application/json") - .header("Authorization", String.format("Bearer %s", AppConfig.API_KEY)) - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) - .build(); - - // Send the request and receive the response - HttpResponse response = client.send( - request, - HttpResponse.BodyHandlers.ofString()); - - // Process the response - String responseBody = response.body(); - return processResponse(responseBody); - } - - private String processResponse(String responseBody) { - JSONObject responseJson = new JSONObject(responseBody); - String generatedImageData = ""; - try { - generatedImageData = responseJson.getJSONArray("data") - .getJSONObject(0).getString("b64_json"); - } catch (Exception chatError) { - // badly formatted json - File file = new File(AppConfig.RECIPE_IMG_FILE); - try { - byte[] imageBytes = Files.readAllBytes(file.toPath()); - generatedImageData = Base64.getEncoder().encodeToString(imageBytes); - } catch (Exception fileError) { - fileError.printStackTrace(); - } - chatError.printStackTrace(); - } - return generatedImageData; - } - - @Override - public byte[] downloadImage(String generatedImageData, String id) { - // convert base64 string to binary data - byte[] generatedImageBytes = Base64.getDecoder().decode(generatedImageData); - try (InputStream in = new ByteArrayInputStream(generatedImageBytes)) { - String path = id + ".jpg"; - Files.copy(in, Paths.get(path)); - } catch (IOException e) { - e.printStackTrace(); - } - return generatedImageBytes; - } - - @Override - public void setError(boolean error) { - } - -} diff --git a/app/src/main/java/code/client/Model/MockDallEService.java b/app/src/main/java/code/client/Model/MockDallEService.java deleted file mode 100644 index b778d92..0000000 --- a/app/src/main/java/code/client/Model/MockDallEService.java +++ /dev/null @@ -1,39 +0,0 @@ -package code.client.Model; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Base64; - -public class MockDallEService extends RecipeToImage { - private boolean error = false; - - @Override - public void setError(boolean error) { - this.error = error; - } - - @Override - public String getResponse(String recipeText) throws IOException, InterruptedException { - if (this.error) { - return "error"; - } else { - File file = new File(AppConfig.RECIPE_IMG_FILE); - // try to give default image - try { - byte[] imageBytes = Files.readAllBytes(file.toPath()); - return Base64.getEncoder().encodeToString(imageBytes); - } catch (Exception fileError) { - fileError.printStackTrace(); - return "RnJpZWQgUmljZSBJbWFnZSA6KQ=="; - } - } - } - - @Override - public byte[] downloadImage(String generatedImageData, String id) { - byte[] generatedImageBytes = Base64.getDecoder().decode(generatedImageData); - return generatedImageBytes; - } - -} diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index 41c1507..583dfce 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -103,7 +103,7 @@ public String performChatGPTRequest(String method, String mealType, String ingre public String performDallERequest(String method, String recipeTitle) { try { - String urlString = AppConfig.SERVER_URL + AppConfig.CHATGPT_PATH; + String urlString = AppConfig.SERVER_URL + AppConfig.DALLE_PATH; urlString += "?=" + recipeTitle; URL url = new URI(urlString).toURL(); diff --git a/app/src/main/java/code/server/DallERequestHandler.java b/app/src/main/java/code/server/DallERequestHandler.java index 203249d..fdbb324 100644 --- a/app/src/main/java/code/server/DallERequestHandler.java +++ b/app/src/main/java/code/server/DallERequestHandler.java @@ -5,8 +5,6 @@ import java.io.*; import java.net.*; - -import java.io.IOException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -29,6 +27,7 @@ public void handle(HttpExchange httpExchange) throws IOException { String query = uri.getRawQuery(); try { String recipeTitle = query.substring(query.indexOf("=") + 1); + recipeTitle = URLEncoder.encode(recipeTitle, "UTF-8"); response = getResponse(recipeTitle); } catch (InterruptedException e) { response = "An error occurred."; @@ -93,7 +92,4 @@ private String processResponse(String responseBody) { return generatedImageData; } - @Override - public void setError(boolean error) { - } } \ No newline at end of file diff --git a/app/src/main/java/code/server/MockDallERequestHandler.java b/app/src/main/java/code/server/MockDallERequestHandler.java index 6db18cf..b2d5d3a 100644 --- a/app/src/main/java/code/server/MockDallERequestHandler.java +++ b/app/src/main/java/code/server/MockDallERequestHandler.java @@ -5,32 +5,20 @@ import java.io.*; import java.net.*; - -import java.io.IOException; import java.nio.file.Files; import java.util.Base64; public class MockDallERequestHandler extends RecipeToImage implements HttpHandler { - private boolean error = false; - - @Override - public void setError(boolean error) { - this.error = error; - } - @Override public void handle(HttpExchange httpExchange) throws IOException { String response = "Request received"; - String method = httpExchange.getRequestMethod(); - System.out.println("Method is " + method); + URI uri = httpExchange.getRequestURI(); + String query = uri.getRawQuery(); try { - if (method.equals("GET")) { - response = handleGet(httpExchange); - } else { - throw new Exception("Not valid request method."); - } + String recipeTitle = query.substring(query.indexOf("=") + 1); + response = getResponse(recipeTitle); } catch (Exception e) { System.out.println("An erroneous request"); e.printStackTrace(); @@ -43,18 +31,6 @@ public void handle(HttpExchange httpExchange) throws IOException { outStream.close(); } - private String handleGet(HttpExchange httpExchange) throws IOException, InterruptedException { - String response = "Invalid GET request"; - URI uri = httpExchange.getRequestURI(); - String query = uri.getRawQuery(); - - if (query != null && !error) { - String recipeTitle = query.substring(query.indexOf("=") + 1); - response = getResponse(recipeTitle); - } - return response; - } - private String getResponse(String recipeText) throws IOException, InterruptedException { File file = new File(AppConfig.RECIPE_IMG_FILE); // try to give default image @@ -63,7 +39,7 @@ private String getResponse(String recipeText) throws IOException, InterruptedExc return Base64.getEncoder().encodeToString(imageBytes); } catch (Exception fileError) { fileError.printStackTrace(); - return "RnJpZWQgUmljZSBJbWFnZSA6KQ=="; + return "RnJpZWQgUmljZSBJbWFnZQ=="; } } diff --git a/app/src/main/java/code/server/MockServer.java b/app/src/main/java/code/server/MockServer.java index 419afb6..3564453 100644 --- a/app/src/main/java/code/server/MockServer.java +++ b/app/src/main/java/code/server/MockServer.java @@ -1,13 +1,49 @@ package code.server; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.sun.net.httpserver.*; + +import code.client.Model.AppConfig; + import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.util.concurrent.*; public class MockServer extends BaseServer { + private IRecipeDb recipeDb; + private AccountMongoDB accountMongoDB; + + private final static int NUM_THREADS = 10; + private HttpServer httpServer; + public MockServer(String hostName, int port) { super(hostName, port); } @Override public void start() throws IOException { + // Initialize a thread pool + ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(NUM_THREADS); + // create a map to store data + // create a server + httpServer = HttpServer.create( + new InetSocketAddress(hostName, port), + 0); + // create the context to map urls + httpServer.createContext(AppConfig.RECIPE_PATH, new RecipeRequestHandler(recipeDb)); + httpServer.createContext(AppConfig.ACCOUNT_PATH, new AccountRequestHandler(accountMongoDB)); + httpServer.createContext(AppConfig.SHARE_PATH, new ShareRequestHandler(accountMongoDB, recipeDb)); + httpServer.createContext(AppConfig.CHATGPT_PATH, new MockChatGPTRequestHandler()); + httpServer.createContext(AppConfig.DALLE_PATH, new MockDallERequestHandler()); + httpServer.createContext(AppConfig.WHISPER_PATH, new MockWhisperRequestHandler()); + // set the executor + httpServer.setExecutor(threadPoolExecutor); + // start the server + httpServer.start(); + System.out.println("Server started on port " + port); } } diff --git a/app/src/main/java/code/server/RecipeToImage.java b/app/src/main/java/code/server/RecipeToImage.java index 9004124..2a57ce3 100644 --- a/app/src/main/java/code/server/RecipeToImage.java +++ b/app/src/main/java/code/server/RecipeToImage.java @@ -6,6 +6,4 @@ public abstract class RecipeToImage { public abstract void handle(HttpExchange httpExchange) throws IOException; - - public abstract void setError(boolean error); } diff --git a/app/src/test/java/code/RecipeToImageTest.java b/app/src/test/java/code/RecipeToImageTest.java index f4513c9..aa3e8d5 100644 --- a/app/src/test/java/code/RecipeToImageTest.java +++ b/app/src/test/java/code/RecipeToImageTest.java @@ -2,20 +2,22 @@ import org.junit.jupiter.api.Test; +import code.server.BaseServer; +import code.server.MockServer; import code.server.Recipe; -import code.client.Model.RecipeToImage; import code.client.Model.AppConfig; -import code.client.Model.MockDallEService; +import code.client.Model.Model; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.Base64; +import java.net.URLEncoder; public class RecipeToImageTest { + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + Model model = new Model(); + /* * Image creation unit tests */ @@ -29,38 +31,14 @@ public void testImageCreation() throws IOException, InterruptedException { recipe.addInstruction("A shrimp fried this rice?"); // DallE request - RecipeToImage recipeToImage = new MockDallEService(); - String imageString = recipeToImage.getResponse(recipe.getTitle()); - // parse base64 to image - byte[] imageBytes = recipeToImage.downloadImage(imageString, recipe.getId()); + server.start(); + String imageString = model.performDallERequest("GET", URLEncoder.encode(recipe.getTitle(), "UTF-8")); + imageString = new String(Base64.getDecoder().decode(imageString)); // default image - File file = new File(AppConfig.RECIPE_IMG_FILE); - String expectedResponse = "Fried Rice Image :)"; - try { - byte[] defaultImageBytes = Files.readAllBytes(file.toPath()); - expectedResponse = Base64.getEncoder().encodeToString(defaultImageBytes); - } catch (Exception fileError) { - fileError.printStackTrace(); - } - - assertEquals(expectedResponse, new String(imageBytes, StandardCharsets.UTF_8)); - } - - @Test - public void testImageCreationError() throws IOException, InterruptedException { - // create a recipe - Recipe recipe = new Recipe("Fried Rice", "Lunch"); - assertEquals("Fried Rice", recipe.getTitle()); - recipe.addIngredient("Rice"); - recipe.addIngredient("Fried"); - recipe.addInstruction("A shrimp fried this rice?"); + String expectedResponse = "Fried Rice Image"; - // DallE request - RecipeToImage recipeToImage = new MockDallEService(); - recipeToImage.setError(true); - String imageString = recipeToImage.getResponse(recipe.getTitle()); - assertEquals("error", imageString); + assertEquals(expectedResponse, imageString); } } \ No newline at end of file From f9a8913bb0231eb840cad9bcb4b8a8c28941b631 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Mon, 4 Dec 2023 19:30:44 -0800 Subject: [PATCH 06/31] refactor: move api request handlers into server --- .../code/client/Controllers/Controller.java | 73 +---------------- .../Format.java} | 13 +-- .../code/client/Model/ChatGPTService.java | 59 -------------- .../code/client/Model/MockGPTService.java | 29 ------- .../code/client/Model/MockHttpConnection.java | 53 ------------- .../code/client/Model/MockWhisperService.java | 25 ------ .../main/java/code/client/Model/Model.java | 17 ++-- .../java/code/client/Model/RecipeToImage.java | 13 --- .../java/code/client/Model/VoiceToText.java | 79 ------------------- .../code/client/Model/WhisperService.java | 77 ------------------ .../java/code/client/View/AppFrameMic.java | 7 -- .../Model => server}/AppHttpConnection.java | 2 +- app/src/main/java/code/server/AppServer.java | 8 ++ app/src/main/java/code/server/BaseServer.java | 2 + .../Model => server}/IHttpConnection.java | 8 +- .../main/java/code/server/TextToRecipe.java | 4 +- .../main/java/code/server/VoiceToText.java | 1 - .../code/server/WhisperRequestHandler.java | 2 +- 18 files changed, 38 insertions(+), 434 deletions(-) rename app/src/main/java/code/client/{Model/TextToRecipe.java => Controllers/Format.java} (89%) delete mode 100644 app/src/main/java/code/client/Model/ChatGPTService.java delete mode 100644 app/src/main/java/code/client/Model/MockGPTService.java delete mode 100644 app/src/main/java/code/client/Model/MockHttpConnection.java delete mode 100644 app/src/main/java/code/client/Model/MockWhisperService.java delete mode 100644 app/src/main/java/code/client/Model/RecipeToImage.java delete mode 100644 app/src/main/java/code/client/Model/VoiceToText.java delete mode 100644 app/src/main/java/code/client/Model/WhisperService.java rename app/src/main/java/code/{client/Model => server}/AppHttpConnection.java (98%) rename app/src/main/java/code/{client/Model => server}/IHttpConnection.java (95%) diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index 7daf227..bc7727d 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -47,6 +47,7 @@ public class Controller { private Account account; private Model model; private View view; + private Format format = new Format(); private IRecipeDb recipeDb; private RecipeCSVWriter recipeWriter; private RecipeCSVReader recipeReader; @@ -57,7 +58,6 @@ public class Controller { // Audio Stuff private boolean recording; // keeps track if the app is currently recording private final AppAudioRecorder recorder = new AppAudioRecorder(); - private VoiceToText voiceToText; private String mealType; // stores the meal type specified by the user private String ingredients; // stores the ingredients listed out by the user @@ -294,7 +294,7 @@ private void handleDetailedViewFromNewRecipeButton(ActionEvent event) { if (mealType != null && ingredients != null) { try { String responseText = model.performChatGPTRequest("GET", mealType, ingredients); - Recipe chatGPTrecipe = mapResponseToRecipe(mealType, responseText); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); chatGPTrecipe.setAccountId(account.getId()); chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); @@ -428,7 +428,7 @@ private void handleRefreshButton(ActionEvent event) { if (mealType != null && ingredients != null) { try { String responseText = model.performChatGPTRequest("GET", mealType, ingredients); - Recipe chatGPTrecipe = mapResponseToRecipe(mealType, responseText); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); chatGPTrecipe.setAccountId(account.getId()); chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); @@ -678,71 +678,4 @@ private void recordIngredients() { /////////////////////////////// AUDIOMANAGEMENT////////////////////////////////// - /* - * TODO: move this to a separate class - */ - public Recipe mapResponseToRecipe(String mealType, String responseText) { - // Split the tokens into lines - String[] tokenArr = responseText.split("\n"); - List tokenList = new ArrayList<>(Arrays.asList(tokenArr)); - int i; - - // Remove empty tokens - for (i = 0; i < tokenList.size();) { - if (tokenList.get(i).isBlank()) { - tokenList.remove(i); - } else { - ++i; - } - } - - // Create a new recipe with a title - Recipe recipe = new Recipe(tokenList.get(0), mealType); - - // Parse recipe's ingredients - String ingredient; - boolean parse = false; - for (i = 0; !tokenList.get(i).contains("Instructions"); ++i) { - ingredient = tokenList.get(i).trim(); - if (ingredient.contains("Ingredients")) { - parse = true; - } else if (parse) { - ingredient = removeDashFromIngredient(tokenList.get(i).trim()); - recipe.addIngredient(ingredient); - } - } - - // Parse recipe's instructions - String instruction; - for (i += 1; i < tokenList.size(); ++i) { - instruction = removeNumberFromInstruction(tokenList.get(i).trim()); - recipe.addInstruction(instruction); - } - - return recipe; - } - - private String removeDashFromIngredient(String ingredient) { - if (ingredient.charAt(0) == ('-')) { - return ingredient.substring(1).trim(); - } - return ingredient.substring(2); - } - - private String removeNumberFromInstruction(String instruction) { - StringBuilder strBuilder = new StringBuilder(); - int i; - - // Ignore characters until '.' - for (i = 0; i < instruction.length() && (instruction.charAt(i) != '.'); ++i) - ; - - // Ignore '.' and ' ' after - // Get all characters until end of string - for (i += 2; i < instruction.length(); ++i) { - strBuilder.append(instruction.charAt(i)); - } - - return strBuilder.toString(); - } } \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/TextToRecipe.java b/app/src/main/java/code/client/Controllers/Format.java similarity index 89% rename from app/src/main/java/code/client/Model/TextToRecipe.java rename to app/src/main/java/code/client/Controllers/Format.java index b9071fd..45974e0 100644 --- a/app/src/main/java/code/client/Model/TextToRecipe.java +++ b/app/src/main/java/code/client/Controllers/Format.java @@ -1,16 +1,11 @@ -package code.client.Model; +package code.client.Controllers; -import java.io.IOException; -import java.net.URISyntaxException; +import code.server.Recipe; import java.util.List; import java.util.ArrayList; import java.util.Arrays; -import code.server.Recipe; - -public abstract class TextToRecipe { - public abstract String getResponse(String mealType, String ingredients) - throws IOException, InterruptedException, URISyntaxException; +public class Format { public String buildPrompt(String mealType, String ingredients) { StringBuilder prompt = new StringBuilder(); prompt.append("I am a student on a budget with a busy schedule and I need to quickly cook a ") @@ -86,6 +81,4 @@ private String removeNumberFromInstruction(String instruction) { return strBuilder.toString(); } - - public abstract void setSampleRecipe(String recipe); } diff --git a/app/src/main/java/code/client/Model/ChatGPTService.java b/app/src/main/java/code/client/Model/ChatGPTService.java deleted file mode 100644 index 886a3fd..0000000 --- a/app/src/main/java/code/client/Model/ChatGPTService.java +++ /dev/null @@ -1,59 +0,0 @@ -package code.client.Model; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import org.json.JSONArray; -import org.json.JSONObject; - -public class ChatGPTService extends TextToRecipe { - public static final String API_ENDPOINT = "https://api.openai.com/v1/completions"; - public static final String MODEL = "text-davinci-003"; - public static final int MAX_TOKENS = 500; - public static final double TEMPERATURE = 1.; - - @Override - public String getResponse(String mealType, String ingredients) - throws IOException, InterruptedException, URISyntaxException { - // Set request parameters - String prompt = buildPrompt(mealType, ingredients); - - // Create a request body which you will pass into request object - JSONObject requestBody = new JSONObject(); - requestBody.put("model", MODEL); - requestBody.put("prompt", prompt); - requestBody.put("max_tokens", MAX_TOKENS); - requestBody.put("temperature", TEMPERATURE); - - // Create the HTTP Client - HttpClient client = HttpClient.newHttpClient(); - - // Create the request object - HttpRequest request = HttpRequest - .newBuilder() - .uri(URI.create(API_ENDPOINT)) - .header("Content-Type", "application/json") - .header("Authorization", String.format("Bearer %s", AppConfig.API_KEY)) - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) - .build(); - - HttpResponse response = client.send( - request, - HttpResponse.BodyHandlers.ofString()); - - // Process the response - String responseBody = response.body(); - JSONObject responseJson = new JSONObject(responseBody); - JSONArray choices = responseJson.getJSONArray("choices"); - String responseText = choices.getJSONObject(0).getString("text"); - return responseText; - } - - @Override - public void setSampleRecipe(String recipe) { - throw new UnsupportedOperationException("Unimplemented method 'setSampleRecipe'"); - } -} \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/MockGPTService.java b/app/src/main/java/code/client/Model/MockGPTService.java deleted file mode 100644 index 46e906b..0000000 --- a/app/src/main/java/code/client/Model/MockGPTService.java +++ /dev/null @@ -1,29 +0,0 @@ -package code.client.Model; - -import java.io.IOException; -import java.net.URISyntaxException; - -public class MockGPTService extends TextToRecipe { - - private String sampleRecipe = """ - Fried Chicken - breakfast - Ingredients: - - 2 chicken breasts, diced - - 2 eggs - Instructions: - 1. Crack 2 eggs into bowl. - 2. Add chicken into bowl and then fry. - 3. Enjoy! - """; - - public void setSampleRecipe(String recipeText) { - sampleRecipe = recipeText; - } - - @Override - public String getResponse(String mealType, String ingredients) - throws IOException, InterruptedException, URISyntaxException { - return sampleRecipe; - } -} diff --git a/app/src/main/java/code/client/Model/MockHttpConnection.java b/app/src/main/java/code/client/Model/MockHttpConnection.java deleted file mode 100644 index 7dd40e1..0000000 --- a/app/src/main/java/code/client/Model/MockHttpConnection.java +++ /dev/null @@ -1,53 +0,0 @@ -package code.client.Model; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class MockHttpConnection implements IHttpConnection { - private int responseCode; - private InputStream inputStream; - private OutputStream outputStream; - - public MockHttpConnection(int responseCode) { - this.responseCode = responseCode; - } - - public MockHttpConnection(int responseCode, InputStream inputStream, OutputStream outputStream) { - this.responseCode = responseCode; - this.inputStream = inputStream; - this.outputStream = outputStream; - } - - @Override - public int getResponseCode() throws IOException { - return responseCode; - } - - @Override - public InputStream getInputStream() { - return inputStream; - } - - @Override - public OutputStream getOutputStream() { - return outputStream; - } - - @Override - public InputStream getErrorStream() { - return inputStream; - } - - @Override - public void setRequestMethod(String method) { - } - - @Override - public void setRequestProperty(String key, String value) { - } - - @Override - public void setDoOutput(boolean output) { - } -} \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/MockWhisperService.java b/app/src/main/java/code/client/Model/MockWhisperService.java deleted file mode 100644 index 034351c..0000000 --- a/app/src/main/java/code/client/Model/MockWhisperService.java +++ /dev/null @@ -1,25 +0,0 @@ -package code.client.Model; - -import java.io.IOException; -import java.net.URISyntaxException; - -public class MockWhisperService extends VoiceToText { - public MockWhisperService() { - super(new MockHttpConnection(200)); - } - - public MockWhisperService(IHttpConnection connection) { - super(connection); - } - - public String processAudio(String type) throws IOException, URISyntaxException { - if (type.equals("mealtype")) { - // processed correctly - return "Breakfast"; - } else if (type.equals("ingredients")) { - return "Chicken, eggs."; - } else { - return "Error text"; - } - } -} diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index 583dfce..bab91ea 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -13,6 +13,7 @@ import java.net.URLConnection; import java.net.URI; import java.nio.file.*; +import java.net.URLEncoder; public class Model { public String performAccountRequest(String method, String user, String password) { @@ -83,18 +84,25 @@ public String performRecipeRequest(String method, String recipe, String userId) public String performChatGPTRequest(String method, String mealType, String ingredients) { try { String urlString = AppConfig.SERVER_URL + AppConfig.CHATGPT_PATH; + mealType = URLEncoder.encode(mealType, "UTF-8"); + ingredients = URLEncoder.encode(ingredients, "UTF-8"); urlString += "?=" + mealType + "::" + ingredients; URL url = new URI(urlString).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); conn.setDoOutput(true); - // System.out.println("Method is " + method); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String response = in.readLine(); + StringBuilder response = new StringBuilder(); + String inputLine; + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + response.append("\n"); + } in.close(); - return response; + + return response.toString(); } catch (Exception ex) { ex.printStackTrace(); return "Error: " + ex.getMessage(); @@ -104,13 +112,12 @@ public String performChatGPTRequest(String method, String mealType, String ingre public String performDallERequest(String method, String recipeTitle) { try { String urlString = AppConfig.SERVER_URL + AppConfig.DALLE_PATH; - urlString += "?=" + recipeTitle; + urlString += "?=" + URLEncoder.encode(recipeTitle, "UTF-8"); URL url = new URI(urlString).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); conn.setDoOutput(true); - // System.out.println("Method is " + method); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String response = in.readLine(); diff --git a/app/src/main/java/code/client/Model/RecipeToImage.java b/app/src/main/java/code/client/Model/RecipeToImage.java deleted file mode 100644 index d883d19..0000000 --- a/app/src/main/java/code/client/Model/RecipeToImage.java +++ /dev/null @@ -1,13 +0,0 @@ -package code.client.Model; - -import java.io.*; - -public abstract class RecipeToImage { - public abstract void setError(boolean error); - - public abstract String getResponse(String recipeTitle) - throws IOException, InterruptedException; - - public abstract byte[] downloadImage(String generatedImageData, String id); - -} diff --git a/app/src/main/java/code/client/Model/VoiceToText.java b/app/src/main/java/code/client/Model/VoiceToText.java deleted file mode 100644 index 457b0d0..0000000 --- a/app/src/main/java/code/client/Model/VoiceToText.java +++ /dev/null @@ -1,79 +0,0 @@ -package code.client.Model; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.URISyntaxException; - -import org.json.JSONException; -import org.json.JSONObject; - -public abstract class VoiceToText { - protected final IHttpConnection connection; - - public VoiceToText(IHttpConnection connection) { - this.connection = connection; - } - - public String processAudio(String type) throws IOException, URISyntaxException { - String response = handleResponse(); - if (type.equals("mealtype")) { - response = response.toUpperCase(); - if (response.contains("BREAKFAST")) { - response = "Breakfast"; - } else if (response.contains("LUNCH")) { - response = "Lunch"; - } else if (response.contains("DINNER")) { - response = "Dinner"; - } else { - response = null; - } - } - return response; - } - - private String handleResponse() throws IOException { - // Get response code - int responseCode = connection.getResponseCode(); - String response; - - // Check response code and handle response accordingly - if (responseCode == HttpURLConnection.HTTP_OK) { - response = handleSuccessResponse(); - } else { - response = handleErrorResponse(); - } - - return response; - } - - // Helper method to handle a successful response - private String handleSuccessResponse() throws IOException, JSONException { - BufferedReader responseReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - String inputLine; - StringBuilder response = new StringBuilder(); - - while ((inputLine = responseReader.readLine()) != null) { - response.append(inputLine); - } - - responseReader.close(); - JSONObject responseJson = new JSONObject(response.toString()); - String generatedText = responseJson.getString("text"); - return generatedText; - } - - // Helper method to handle an error response - private String handleErrorResponse() throws IOException, JSONException { - BufferedReader errorReader = new BufferedReader(new InputStreamReader(connection.getErrorStream())); - String errorLine; - StringBuilder errorResponse = new StringBuilder(); - - while ((errorLine = errorReader.readLine()) != null) { - errorResponse.append(errorLine); - } - - errorReader.close(); - String errorResult = errorResponse.toString(); - return errorResult; - } -} diff --git a/app/src/main/java/code/client/Model/WhisperService.java b/app/src/main/java/code/client/Model/WhisperService.java deleted file mode 100644 index 02c76e0..0000000 --- a/app/src/main/java/code/client/Model/WhisperService.java +++ /dev/null @@ -1,77 +0,0 @@ -package code.client.Model; - -import java.io.*; -import java.net.URISyntaxException; - -public class WhisperService extends VoiceToText { - public static final String API_ENDPOINT = "https://api.openai.com/v1/audio/transcriptions"; - public static final String MODEL = "whisper-1"; - - public WhisperService() throws URISyntaxException, IOException { - super(new AppHttpConnection(API_ENDPOINT)); - } - - // https://stackoverflow.com/questions/25334139/how-to-mock-a-url-connection - public String processAudio(String type) throws IOException, URISyntaxException { - // Send HTTP request - sendHttpRequest(); - return super.processAudio(type); - } - - private void sendHttpRequest() throws IOException, URISyntaxException { - // Set up request headers - File file = new File(AppConfig.AUDIO_FILE); - String boundary = "Boundary-" + System.currentTimeMillis(); - connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); - connection.setRequestProperty("Authorization", "Bearer " + AppConfig.API_KEY); - connection.setRequestMethod("POST"); - connection.setDoOutput(true); - // Set up output stream to write request body - OutputStream outputStream = connection.getOutputStream(); - // Write model parameter to request body - writeParameterToOutputStream(outputStream, "model", MODEL, boundary); - // Write file parameter to request body - writeFileToOutputStream(outputStream, file, boundary); - // Write closing boundary to request body - outputStream.write(("\r\n--" + boundary + "--\r\n").getBytes()); - // Flush and close output stream - outputStream.flush(); - outputStream.close(); - } - - // Helper method to write a parameter to the output stream in multipart form - // data format - private static void writeParameterToOutputStream( - OutputStream outputStream, - String parameterName, - String parameterValue, - String boundary) throws IOException { - outputStream.write(("--" + boundary + "\r\n").getBytes()); - outputStream.write( - ("Content-Disposition: form-data; name=\"" + parameterName + "\"\r\n\r\n").getBytes()); - outputStream.write((parameterValue + "\r\n").getBytes()); - } - - // Helper method to write a file to the output stream in multipart form data - // format - private static void writeFileToOutputStream( - OutputStream outputStream, - File file, - String boundary) throws IOException { - outputStream.write(("--" + boundary + "\r\n").getBytes()); - outputStream.write(("Content-Disposition: form-data; name=\"file\"; filename=\"" + - file.getName() + - "\"\r\n").getBytes()); - outputStream.write(("Content-Type: audio/mpeg\r\n\r\n").getBytes()); - - FileInputStream fileInputStream = new FileInputStream(file); - byte[] buffer = new byte[1024]; - int bytesRead; - - while ((bytesRead = fileInputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - - fileInputStream.close(); - } -} diff --git a/app/src/main/java/code/client/View/AppFrameMic.java b/app/src/main/java/code/client/View/AppFrameMic.java index eb21593..6916ded 100644 --- a/app/src/main/java/code/client/View/AppFrameMic.java +++ b/app/src/main/java/code/client/View/AppFrameMic.java @@ -4,20 +4,13 @@ import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Pos; -import java.io.File; import java.io.IOException; import java.net.URISyntaxException; -import java.util.ArrayList; import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.text.*; -import code.client.View.Ingredients; -import code.client.View.MealType; -// import javax.sound.sampled.*; class GPTRecipe extends GridPane { private Label recipeLabel; diff --git a/app/src/main/java/code/client/Model/AppHttpConnection.java b/app/src/main/java/code/server/AppHttpConnection.java similarity index 98% rename from app/src/main/java/code/client/Model/AppHttpConnection.java rename to app/src/main/java/code/server/AppHttpConnection.java index dc9e599..30345fd 100644 --- a/app/src/main/java/code/client/Model/AppHttpConnection.java +++ b/app/src/main/java/code/server/AppHttpConnection.java @@ -1,4 +1,4 @@ -package code.client.Model; +package code.server; import java.net.HttpURLConnection; import java.net.ProtocolException; diff --git a/app/src/main/java/code/server/AppServer.java b/app/src/main/java/code/server/AppServer.java index 8c5ef73..0b55f24 100644 --- a/app/src/main/java/code/server/AppServer.java +++ b/app/src/main/java/code/server/AppServer.java @@ -58,4 +58,12 @@ public void start() throws IOException { httpServer.start(); System.out.println("Server started on port " + port); } + + @Override + public void stop() { + if (httpServer != null) { + httpServer.stop(0); // Stop the server gracefully + System.out.println("Server stopped"); + } + } } diff --git a/app/src/main/java/code/server/BaseServer.java b/app/src/main/java/code/server/BaseServer.java index 080def9..65edad6 100644 --- a/app/src/main/java/code/server/BaseServer.java +++ b/app/src/main/java/code/server/BaseServer.java @@ -20,4 +20,6 @@ public int getPort() { } public abstract void start() throws IOException; + + public abstract void stop(); } diff --git a/app/src/main/java/code/client/Model/IHttpConnection.java b/app/src/main/java/code/server/IHttpConnection.java similarity index 95% rename from app/src/main/java/code/client/Model/IHttpConnection.java rename to app/src/main/java/code/server/IHttpConnection.java index 7eaf56b..604d9b0 100644 --- a/app/src/main/java/code/client/Model/IHttpConnection.java +++ b/app/src/main/java/code/server/IHttpConnection.java @@ -1,4 +1,4 @@ -package code.client.Model; +package code.server; import java.net.ProtocolException; import java.io.IOException; @@ -7,10 +7,16 @@ public interface IHttpConnection { int getResponseCode() throws IOException; + InputStream getInputStream() throws IOException; + OutputStream getOutputStream() throws IOException; + InputStream getErrorStream() throws IOException; + void setRequestMethod(String method) throws ProtocolException; + void setRequestProperty(String key, String value); + void setDoOutput(boolean output); } \ No newline at end of file diff --git a/app/src/main/java/code/server/TextToRecipe.java b/app/src/main/java/code/server/TextToRecipe.java index c2cc29b..53b2793 100644 --- a/app/src/main/java/code/server/TextToRecipe.java +++ b/app/src/main/java/code/server/TextToRecipe.java @@ -2,9 +2,6 @@ import com.sun.net.httpserver.*; import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import java.util.Arrays; public abstract class TextToRecipe { public abstract void handle(HttpExchange httpExchange) throws IOException; @@ -21,4 +18,5 @@ public String buildPrompt(String mealType, String ingredients) { } public abstract void setSampleRecipe(String recipe); + } diff --git a/app/src/main/java/code/server/VoiceToText.java b/app/src/main/java/code/server/VoiceToText.java index 36bffdb..2fb1ffa 100644 --- a/app/src/main/java/code/server/VoiceToText.java +++ b/app/src/main/java/code/server/VoiceToText.java @@ -6,7 +6,6 @@ import org.json.JSONException; import org.json.JSONObject; -import code.client.Model.IHttpConnection; public abstract class VoiceToText { protected final IHttpConnection connection; diff --git a/app/src/main/java/code/server/WhisperRequestHandler.java b/app/src/main/java/code/server/WhisperRequestHandler.java index c3a7e7b..00bec3c 100644 --- a/app/src/main/java/code/server/WhisperRequestHandler.java +++ b/app/src/main/java/code/server/WhisperRequestHandler.java @@ -3,7 +3,7 @@ import com.sun.net.httpserver.*; import code.client.Model.AppConfig; -import code.client.Model.AppHttpConnection; + import java.io.*; import java.net.*; From d08319a8c8c4985ceb3995063bb49af3feb3b1f2 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Mon, 4 Dec 2023 19:30:53 -0800 Subject: [PATCH 07/31] ci: mock server/api request --- .../server/MockChatGPTRequestHandler.java | 15 +++ .../java/code/server/MockHttpConnection.java | 53 ++++++++ app/src/main/java/code/server/MockServer.java | 7 ++ .../server/MockWhisperRequestHandler.java | 24 +++- .../java/code/GetRecipeListForUserTest.java | 114 +++++++++--------- app/src/test/java/code/RecipeToImageTest.java | 3 +- app/src/test/java/code/RefreshTest.java | 95 +++++++-------- .../test/java/code/ServerConnectionTest.java | 11 +- app/src/test/java/code/TextToRecipeTest.java | 80 ++++++------ app/src/test/java/code/VoiceToTextTest.java | 36 ++---- 10 files changed, 256 insertions(+), 182 deletions(-) create mode 100644 app/src/main/java/code/server/MockHttpConnection.java diff --git a/app/src/main/java/code/server/MockChatGPTRequestHandler.java b/app/src/main/java/code/server/MockChatGPTRequestHandler.java index 8ded3e2..edf2205 100644 --- a/app/src/main/java/code/server/MockChatGPTRequestHandler.java +++ b/app/src/main/java/code/server/MockChatGPTRequestHandler.java @@ -25,6 +25,21 @@ public void setSampleRecipe(String recipeText) { @Override public void handle(HttpExchange httpExchange) throws IOException { + String method = httpExchange.getRequestMethod(); + if (method.equals("GET2")) { + sampleRecipe = """ + Fried Chicken and Egg Fried Rice + breakfast + Ingredients: + + - 2 chicken breasts, diced + - 2 large eggs + - 2 cups cooked rice + - 2 tablespoons vegetable oil + - 2 tablespoons soy sauce + - 1 teaspoon sesame oil + - Salt and pepper to taste"""; + } httpExchange.sendResponseHeaders(200, sampleRecipe.length()); OutputStream outStream = httpExchange.getResponseBody(); outStream.write(sampleRecipe.getBytes()); diff --git a/app/src/main/java/code/server/MockHttpConnection.java b/app/src/main/java/code/server/MockHttpConnection.java new file mode 100644 index 0000000..bea2bbc --- /dev/null +++ b/app/src/main/java/code/server/MockHttpConnection.java @@ -0,0 +1,53 @@ +package code.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class MockHttpConnection implements IHttpConnection { + private int responseCode; + private InputStream inputStream; + private OutputStream outputStream; + + public MockHttpConnection(int responseCode) { + this.responseCode = responseCode; + } + + public MockHttpConnection(int responseCode, InputStream inputStream, OutputStream outputStream) { + this.responseCode = responseCode; + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int getResponseCode() throws IOException { + return responseCode; + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public OutputStream getOutputStream() { + return outputStream; + } + + @Override + public InputStream getErrorStream() { + return inputStream; + } + + @Override + public void setRequestMethod(String method) { + } + + @Override + public void setRequestProperty(String key, String value) { + } + + @Override + public void setDoOutput(boolean output) { + } +} \ No newline at end of file diff --git a/app/src/main/java/code/server/MockServer.java b/app/src/main/java/code/server/MockServer.java index 3564453..32f7dcc 100644 --- a/app/src/main/java/code/server/MockServer.java +++ b/app/src/main/java/code/server/MockServer.java @@ -46,4 +46,11 @@ public void start() throws IOException { httpServer.start(); System.out.println("Server started on port " + port); } + + public void stop() { + if (httpServer != null) { + httpServer.stop(0); // Stop the server gracefully + System.out.println("Server stopped"); + } + } } diff --git a/app/src/main/java/code/server/MockWhisperRequestHandler.java b/app/src/main/java/code/server/MockWhisperRequestHandler.java index 47a4b79..f19c8eb 100644 --- a/app/src/main/java/code/server/MockWhisperRequestHandler.java +++ b/app/src/main/java/code/server/MockWhisperRequestHandler.java @@ -1,12 +1,11 @@ package code.server; +import com.sun.net.httpserver.*; import java.io.IOException; +import java.io.OutputStream; import java.net.URISyntaxException; -import code.client.Model.IHttpConnection; -import code.client.Model.MockHttpConnection; - -public class MockWhisperRequestHandler extends VoiceToText { +public class MockWhisperRequestHandler extends VoiceToText implements HttpHandler { public MockWhisperRequestHandler() { super(new MockHttpConnection(200)); } @@ -25,4 +24,21 @@ public String processAudio(String type) throws IOException, URISyntaxException { return "Error text"; } } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + String method = httpExchange.getRequestMethod(); + String response = "Request received"; + if (method.equals("GET")) { + response = "Breakfast"; + } else if (method.equals("GET2")) { + response = "Chicken, eggs."; + } + + // Sending back response to the client + httpExchange.sendResponseHeaders(200, response.length()); + OutputStream outStream = httpExchange.getResponseBody(); + outStream.write(response.getBytes()); + outStream.close(); + } } diff --git a/app/src/test/java/code/GetRecipeListForUserTest.java b/app/src/test/java/code/GetRecipeListForUserTest.java index 08d05fd..cf10313 100644 --- a/app/src/test/java/code/GetRecipeListForUserTest.java +++ b/app/src/test/java/code/GetRecipeListForUserTest.java @@ -1,65 +1,69 @@ -package code; +// package code; -import org.junit.jupiter.api.Test; +// import org.junit.jupiter.api.Test; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoDatabase; +// import com.mongodb.client.MongoClient; +// import com.mongodb.client.MongoClients; +// import com.mongodb.client.MongoCollection; +// import com.mongodb.client.MongoDatabase; -import org.bson.Document; -import org.junit.jupiter.api.BeforeEach; +// import org.bson.Document; +// import org.junit.jupiter.api.BeforeEach; -import static org.junit.jupiter.api.Assertions.*; +// import static org.junit.jupiter.api.Assertions.*; -import code.server.*; -import code.client.Model.AppConfig; -import code.client.Model.ChatGPTService; -import code.client.Model.MockGPTService; -import code.client.Model.TextToRecipe; +// import code.server.*; +// import code.client.Model.AppConfig; +// import code.client.Model.ChatGPTService; +// import code.client.Model.MockGPTService; +// import code.client.Model.TextToRecipe; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.List; +// import java.io.IOException; +// import java.net.URISyntaxException; +// import java.util.List; -public class GetRecipeListForUserTest { - private Account accountWithOne; - private Account accountWithNone; - private IRecipeDb recipeDb; +// public class GetRecipeListForUserTest { +// private Account accountWithOne; +// private Account accountWithNone; +// private IRecipeDb recipeDb; - @BeforeEach - public void setUp() throws IOException { - accountWithOne = new Account("656a2e6d8a659b00c86888b8", "chris", "chris"); - accountWithNone = new Account("6567dcbcb954ce6c58955ee0", "allen", "allen"); - } +// @BeforeEach +// public void setUp() throws IOException { +// accountWithOne = new Account("656a2e6d8a659b00c86888b8", "chris", "chris"); +// accountWithNone = new Account("6567dcbcb954ce6c58955ee0", "allen", "allen"); +// } - @Test - public void findSingleRecipeStored() throws IOException, InterruptedException, URISyntaxException { - TextToRecipe mockResponse = new MockGPTService(); - String storedRecipeStr = mockResponse.getResponse("", ""); - Recipe storedRecipe = mockResponse.mapResponseToRecipe("breakfast", storedRecipeStr); - try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { - MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); - MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); - recipeDb = new RecipeMongoDb(recipeCollection); - List recipeList = recipeDb.getList(accountWithOne.getId()); - assertTrue(recipeList.size() == 1); - assertEquals(storedRecipe, recipeList.get(0)); - } catch (Exception e) { - e.printStackTrace(); - } - } +// @Test +// public void findSingleRecipeStored() throws IOException, +// InterruptedException, URISyntaxException { +// TextToRecipe mockResponse = new MockGPTService(); +// String storedRecipeStr = mockResponse.getResponse("", ""); +// Recipe storedRecipe = mockResponse.mapResponseToRecipe("breakfast", +// storedRecipeStr); +// try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { +// MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); +// MongoCollection recipeCollection = +// mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); +// recipeDb = new RecipeMongoDb(recipeCollection); +// List recipeList = recipeDb.getList(accountWithOne.getId()); +// assertTrue(recipeList.size() == 1); +// assertEquals(storedRecipe, recipeList.get(0)); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } - @Test - public void findNoRecipeStored() { - try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { - MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); - MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); - recipeDb = new RecipeMongoDb(recipeCollection); - List recipeList = recipeDb.getList(accountWithNone.getId()); - assertTrue(recipeList.isEmpty()); - } catch (Exception e) { - e.printStackTrace(); - } - } -} +// @Test +// public void findNoRecipeStored() { +// try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { +// MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); +// MongoCollection recipeCollection = +// mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); +// recipeDb = new RecipeMongoDb(recipeCollection); +// List recipeList = recipeDb.getList(accountWithNone.getId()); +// assertTrue(recipeList.isEmpty()); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } +// } diff --git a/app/src/test/java/code/RecipeToImageTest.java b/app/src/test/java/code/RecipeToImageTest.java index aa3e8d5..5b44a5f 100644 --- a/app/src/test/java/code/RecipeToImageTest.java +++ b/app/src/test/java/code/RecipeToImageTest.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.util.Base64; -import java.net.URLEncoder; public class RecipeToImageTest { BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); @@ -32,7 +31,7 @@ public void testImageCreation() throws IOException, InterruptedException { // DallE request server.start(); - String imageString = model.performDallERequest("GET", URLEncoder.encode(recipe.getTitle(), "UTF-8")); + String imageString = model.performDallERequest("GET", recipe.getTitle()); imageString = new String(Base64.getDecoder().decode(imageString)); // default image diff --git a/app/src/test/java/code/RefreshTest.java b/app/src/test/java/code/RefreshTest.java index 4e63ef0..306cf7f 100644 --- a/app/src/test/java/code/RefreshTest.java +++ b/app/src/test/java/code/RefreshTest.java @@ -1,47 +1,48 @@ -package code; - -import code.client.Controllers.Controller; -import code.client.Model.MockGPTService; -import code.client.Model.MockDallEService; -import code.client.Model.Model; -import code.client.View.DetailsAppFrame; -import code.client.View.View; -import javafx.scene.control.Button; -import javafx.stage.Stage; -import org.junit.jupiter.api.Test; -import code.client.Model.Account; -import code.client.Model.AccountCSVReader; -import code.client.Model.AccountCSVWriter; -import code.client.Model.ChatGPTService; -import code.client.View.*; -import code.client.Controllers.*; -import code.server.*; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.*; - -public class RefreshTest { - @Test - public void refreshTesting() { - DetailsAppFrame detailsAppFrame = new DetailsAppFrame(); - // TextToRecipe txt = new TextToRecipe(); - // initial recipe before refresh - Recipe initialRecipe = detailsAppFrame.getDisplayedRecipe(); - - MockGPTService mockGPT = new MockGPTService(); - Recipe refreshedRecipe; - } - - private Recipe generateRefresh(Recipe originalRecipe, MockGPTService mockGPTService) - throws IOException, InterruptedException, URISyntaxException { - String mealType = "Breakfast"; - String ingredients = "Chicken, eggs."; - - String refreshedResponse = mockGPTService.getResponse(mealType, ingredients); - Recipe out = mockGPTService.mapResponseToRecipe(mealType, refreshedResponse); - - return out; - } - -} \ No newline at end of file +// package code; + +// import code.client.Controllers.Controller; +// import code.client.Model.MockGPTService; +// import code.client.Model.MockDallEService; +// import code.client.Model.Model; +// import code.client.View.DetailsAppFrame; +// import code.client.View.View; +// import javafx.scene.control.Button; +// import javafx.stage.Stage; +// import org.junit.jupiter.api.Test; +// import code.client.Model.Account; +// import code.client.Model.AccountCSVReader; +// import code.client.Model.AccountCSVWriter; +// import code.client.Model.ChatGPTService; +// import code.client.View.*; +// import code.client.Controllers.*; +// import code.server.*; + +// import java.io.IOException; +// import java.net.URISyntaxException; +// import java.util.*; + +// public class RefreshTest { +// @Test +// public void refreshTesting() { +// DetailsAppFrame detailsAppFrame = new DetailsAppFrame(); +// // TextToRecipe txt = new TextToRecipe(); +// // initial recipe before refresh +// Recipe initialRecipe = detailsAppFrame.getDisplayedRecipe(); + +// MockGPTService mockGPT = new MockGPTService(); +// Recipe refreshedRecipe; +// } + +// private Recipe generateRefresh(Recipe originalRecipe, MockGPTService +// mockGPTService) +// throws IOException, InterruptedException, URISyntaxException { +// String mealType = "Breakfast"; +// String ingredients = "Chicken, eggs."; + +// String refreshedResponse = mockGPTService.getResponse(mealType, ingredients); +// Recipe out = mockGPTService.mapResponseToRecipe(mealType, refreshedResponse); + +// return out; +// } + +// } \ No newline at end of file diff --git a/app/src/test/java/code/ServerConnectionTest.java b/app/src/test/java/code/ServerConnectionTest.java index 69c8e7d..490363c 100644 --- a/app/src/test/java/code/ServerConnectionTest.java +++ b/app/src/test/java/code/ServerConnectionTest.java @@ -6,6 +6,7 @@ import code.client.Model.AppConfig; import code.client.View.ServerConnection; +import code.server.BaseServer; import code.server.MockServer; import java.io.IOException; @@ -28,8 +29,8 @@ public void restoreStreams() { } @Test - void testServerOffline() { - MockServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + void testServerOffline() throws IOException { + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); ServerConnection connection = new ServerConnection(server); assertFalse(connection.isOnline()); assertEquals("Server is offline", outData.toString()); @@ -37,11 +38,11 @@ void testServerOffline() { @Test void testServerOnline() throws IOException { - // Googles public DNS : should be true - MockServer server = new MockServer("8.8.8.8", 443); + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); ServerConnection connection = new ServerConnection(server); server.start(); assertTrue(connection.isOnline()); - assertEquals("Server is online", outData.toString()); + assertTrue(outData.toString().contains("Server is online")); + server.stop(); } } diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index 1d9699e..2080b37 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -2,13 +2,10 @@ import org.junit.jupiter.api.Test; -import code.client.Model.TextToRecipe; -import code.client.Model.VoiceToText; +import code.client.Controllers.Format; +import code.client.Model.AppConfig; +import code.client.Model.Model; import code.server.*; -import code.client.Model.MockDallEService; -import code.client.Model.MockGPTService; -import code.client.Model.MockWhisperService; -import code.client.Model.RecipeToImage; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -18,31 +15,37 @@ import java.net.URISyntaxException; public class TextToRecipeTest { + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + Model model = new Model(); + Format format = new Format(); @Test /** * Integration test for provide recipe */ - public void testProvideRecipeIntegration() throws IOException, URISyntaxException, InterruptedException { + public void testProvideRecipeIntegration() throws IOException, + URISyntaxException, InterruptedException { // record and process audio - VoiceToText voiceToText = new MockWhisperService(); - String mealType = voiceToText.processAudio("mealtype"); - String ingredients = voiceToText.processAudio("ingredients"); + server.start(); + String mealType = "Breakfast"; // model.performWhisperRequest("GET", "mealtype"); + String ingredients = "Chicken, eggs."; // model.performWhisperRequest("GET2", "ingredients"); // build prompt for chatGPT - TextToRecipe textToRecipe = new MockGPTService(); String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Breakfast. Chicken, eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; - String response = textToRecipe.buildPrompt(mealType, ingredients); + String response = format.buildPrompt(mealType, ingredients); assertEquals(prompt, response); // create Recipe object from response - String responseText = textToRecipe.getResponse(mealType, ingredients); - Recipe chatGPTrecipe = textToRecipe.mapResponseToRecipe(mealType, responseText); - RecipeToImage recipeToImage = new MockDallEService(); - chatGPTrecipe.setImage(recipeToImage.getResponse(chatGPTrecipe.getTitle())); + String responseText = model.performChatGPTRequest("GET", mealType, ingredients); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, + responseText); + + chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); + assertEquals("Fried Chicken", chatGPTrecipe.getTitle()); assertEquals("Breakfast", chatGPTrecipe.getMealTag()); assertNotNull(chatGPTrecipe.getImage()); + server.stop(); } /** @@ -50,16 +53,19 @@ public void testProvideRecipeIntegration() throws IOException, URISyntaxExceptio */ @Test public void testPromptBuild() { - TextToRecipe textToRecipe = new MockGPTService(); + TextToRecipe textToRecipe = new MockChatGPTRequestHandler(); String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Lunch. I have rice, shrimp, chicken, and eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; String response = textToRecipe.buildPrompt("Lunch", "I have rice, shrimp, chicken, and eggs."); assertEquals(prompt, response); } @Test - public void testRefreshRecipe() throws IOException, InterruptedException, URISyntaxException { - TextToRecipe textToRecipe = new MockGPTService(); - String initialResponse = textToRecipe.getResponse("breakfast", "chicken, eggs"); + public void testRefreshRecipe() throws IOException, InterruptedException, + URISyntaxException { + server.start(); + String mealType = "breakfast"; + String ingredients = "chicken, eggs"; + String initialResponse = model.performChatGPTRequest("GET", mealType, ingredients); String expectedResponse = """ Fried Chicken breakfast @@ -74,25 +80,13 @@ public void testRefreshRecipe() throws IOException, InterruptedException, URISyn assertEquals(expectedResponse, initialResponse); // simulate refresh - textToRecipe.setSampleRecipe(""" - Fried Chicken and Egg Fried Rice - breakfast - Ingredients: - - - 2 chicken breasts, diced - - 2 large eggs - - 2 cups cooked rice - - 2 tablespoons vegetable oil - - 2 tablespoons soy sauce - - 1 teaspoon sesame oil - - Salt and pepper to taste"""); - String refreshResponse = textToRecipe.getResponse("breakfast", "chicken, eggs"); + String refreshResponse = model.performChatGPTRequest("GET2", mealType, ingredients); assertNotEquals(initialResponse, refreshResponse); + server.stop(); } @Test public void testParseJSON() { - TextToRecipe textToRecipe = new MockGPTService(); String mealType = "BREAKFAST"; String responseText = """ Fried Chicken and Egg Fried Rice @@ -146,13 +140,12 @@ public void testParseJSON() { Cook the fried rice until everything is heated through, about 5 minutes. Drizzle with sesame oil, season with more salt and pepper if desired, and serve. Enjoy! """; - Recipe recipe = textToRecipe.mapResponseToRecipe(mealType, responseText); + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); assertEquals(parsedResponse, recipe.toString()); } @Test public void testParseNoIndentsAndNewLines() { - TextToRecipe textToRecipe = new MockGPTService(); String mealType = "LUNCH"; String responseText = """ Cheesy pasta bake @@ -208,13 +201,12 @@ public void testParseNoIndentsAndNewLines() { Top with shredded cheese and bake in preheated oven for 25-30 minutes, or until the cheese is melted and bubbly. Serve the cheesy pasta bake with garlic bread. Enjoy! """; - Recipe recipe = textToRecipe.mapResponseToRecipe(mealType, responseText); + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); assertEquals(parsedResponse, recipe.toString()); } @Test public void testParseDifferentLineSpacing() { - TextToRecipe textToRecipe = new MockGPTService(); String mealType = "DINNER"; String responseText = """ @@ -253,8 +245,7 @@ public void testParseDifferentLineSpacing() { 10. Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. - 11. Enjoy! - """; + 11. Enjoy!"""; String parsedResponse = """ Title: Savory Beef Pasta Bake Meal tag: DINNER @@ -281,17 +272,15 @@ public void testParseDifferentLineSpacing() { Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. Enjoy! """; - Recipe recipe = textToRecipe.mapResponseToRecipe(mealType, responseText); + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); assertEquals(parsedResponse, recipe.toString()); } @Test public void testParseRemoveDashesAndNumbers() { - TextToRecipe textToRecipe = new MockGPTService(); String mealType = "LUNCH"; String responseText = """ - Savory Beef Pasta Bake LUNCH Ingredients: @@ -327,8 +316,7 @@ public void testParseRemoveDashesAndNumbers() { 10. Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. - 11. Enjoy! - """; + 11. Enjoy!"""; String parsedResponse = """ Title: Savory Beef Pasta Bake Meal tag: LUNCH @@ -355,7 +343,7 @@ public void testParseRemoveDashesAndNumbers() { Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. Enjoy! """; - Recipe recipe = textToRecipe.mapResponseToRecipe(mealType, responseText); + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); assertEquals(parsedResponse, recipe.toString()); } } diff --git a/app/src/test/java/code/VoiceToTextTest.java b/app/src/test/java/code/VoiceToTextTest.java index ffd97f8..ba8005d 100644 --- a/app/src/test/java/code/VoiceToTextTest.java +++ b/app/src/test/java/code/VoiceToTextTest.java @@ -5,42 +5,32 @@ import org.junit.jupiter.api.Test; -import code.client.Model.IHttpConnection; -import code.client.Model.MockHttpConnection; -import code.client.Model.MockWhisperService; -import code.client.Model.VoiceToText; +import code.client.Model.AppConfig; +import code.client.Model.Model; +import code.server.BaseServer; +import code.server.IHttpConnection; +import code.server.MockHttpConnection; +import code.server.MockServer; import static org.junit.jupiter.api.Assertions.assertEquals; public class VoiceToTextTest { + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + Model model = new Model(); + /* - * Integration Test + * Integration Test for processing both audios */ @Test void testSuccessfulProcessAudio() throws IOException, URISyntaxException { - IHttpConnection connection = new MockHttpConnection(200); - - VoiceToText voiceToText = new MockWhisperService(connection); - String response = voiceToText.processAudio("mealtype"); + server.start(); + String response = model.performWhisperRequest("GET", "mealType"); assertEquals("Breakfast", response); - voiceToText = new MockWhisperService(connection); - response = voiceToText.processAudio("ingredients"); + response = model.performWhisperRequest("GET2", "ingredients"); assertEquals("Chicken, eggs.", response); } - /* - * Unit test - */ - @Test - void testFailedProcessAudio() throws IOException, URISyntaxException { - IHttpConnection connection = new MockHttpConnection(404); - - VoiceToText voiceToText = new MockWhisperService(connection); - String response = voiceToText.processAudio("error"); - assertEquals("Error text", response); - } - /* * Unit test */ From d20dea04403654d3d22a828091da26cda185b9bc Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Mon, 4 Dec 2023 19:44:38 -0800 Subject: [PATCH 08/31] ci: add server.stop() to prevent port binding --- app/src/main/java/code/server/MockServer.java | 1 + app/src/test/java/code/RecipeToImageTest.java | 1 + app/src/test/java/code/TextToRecipeTest.java | 663 +++++++++--------- 3 files changed, 334 insertions(+), 331 deletions(-) diff --git a/app/src/main/java/code/server/MockServer.java b/app/src/main/java/code/server/MockServer.java index 32f7dcc..be019da 100644 --- a/app/src/main/java/code/server/MockServer.java +++ b/app/src/main/java/code/server/MockServer.java @@ -47,6 +47,7 @@ public void start() throws IOException { System.out.println("Server started on port " + port); } + @Override public void stop() { if (httpServer != null) { httpServer.stop(0); // Stop the server gracefully diff --git a/app/src/test/java/code/RecipeToImageTest.java b/app/src/test/java/code/RecipeToImageTest.java index 5b44a5f..2cbf815 100644 --- a/app/src/test/java/code/RecipeToImageTest.java +++ b/app/src/test/java/code/RecipeToImageTest.java @@ -38,6 +38,7 @@ public void testImageCreation() throws IOException, InterruptedException { String expectedResponse = "Fried Rice Image"; assertEquals(expectedResponse, imageString); + server.stop(); } } \ No newline at end of file diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index 2080b37..f3a8abd 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -15,335 +15,336 @@ import java.net.URISyntaxException; public class TextToRecipeTest { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); - Model model = new Model(); - Format format = new Format(); - - @Test - /** - * Integration test for provide recipe - */ - public void testProvideRecipeIntegration() throws IOException, - URISyntaxException, InterruptedException { - // record and process audio - server.start(); - String mealType = "Breakfast"; // model.performWhisperRequest("GET", "mealtype"); - String ingredients = "Chicken, eggs."; // model.performWhisperRequest("GET2", "ingredients"); - - // build prompt for chatGPT - String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Breakfast. Chicken, eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; - String response = format.buildPrompt(mealType, ingredients); - assertEquals(prompt, response); - - // create Recipe object from response - String responseText = model.performChatGPTRequest("GET", mealType, ingredients); - Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, - responseText); - - chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); - - assertEquals("Fried Chicken", chatGPTrecipe.getTitle()); - assertEquals("Breakfast", chatGPTrecipe.getMealTag()); - assertNotNull(chatGPTrecipe.getImage()); - server.stop(); - } - - /** - * Unit tests for Recipe features - */ - @Test - public void testPromptBuild() { - TextToRecipe textToRecipe = new MockChatGPTRequestHandler(); - String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Lunch. I have rice, shrimp, chicken, and eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; - String response = textToRecipe.buildPrompt("Lunch", "I have rice, shrimp, chicken, and eggs."); - assertEquals(prompt, response); - } - - @Test - public void testRefreshRecipe() throws IOException, InterruptedException, - URISyntaxException { - server.start(); - String mealType = "breakfast"; - String ingredients = "chicken, eggs"; - String initialResponse = model.performChatGPTRequest("GET", mealType, ingredients); - String expectedResponse = """ - Fried Chicken - breakfast - Ingredients: - - 2 chicken breasts, diced - - 2 eggs - Instructions: - 1. Crack 2 eggs into bowl. - 2. Add chicken into bowl and then fry. - 3. Enjoy! - """; - assertEquals(expectedResponse, initialResponse); - - // simulate refresh - String refreshResponse = model.performChatGPTRequest("GET2", mealType, ingredients); - assertNotEquals(initialResponse, refreshResponse); - server.stop(); - } - - @Test - public void testParseJSON() { - String mealType = "BREAKFAST"; - String responseText = """ - Fried Chicken and Egg Fried Rice - BREAKFAST - Ingredients: - - - 2 chicken breasts, diced - - 2 large eggs - - 2 cups cooked rice - - 2 tablespoons vegetable oil - - 2 tablespoons soy sauce - - 1 teaspoon sesame oil - - Salt and pepper to taste - - Instructions: - - 1. Heat the vegetable oil in a large pan over medium-high heat. - - 2. Add the diced chicken and cook until the chicken is cooked through, about 8 minutes. - - 3. In a separate bowl, beat the eggs and season with salt and pepper. - - 4. Reduce the heat to medium and add the eggs to the pan with the chicken. - - 5. Using a spatula, scramble the eggs until they are cooked through. - - 6. Add in the cooked rice and soy sauce and stir to combine with the chicken and eggs. - - 7. Cook the fried rice until everything is heated through, about 5 minutes. - - 8. Drizzle with sesame oil, season with more salt and pepper if desired, and serve. Enjoy! - """; - String parsedResponse = """ - Title: Fried Chicken and Egg Fried Rice - Meal tag: BREAKFAST - Ingredients: - 2 chicken breasts, diced - 2 large eggs - 2 cups cooked rice - 2 tablespoons vegetable oil - 2 tablespoons soy sauce - 1 teaspoon sesame oil - Salt and pepper to taste - Instructions: - Heat the vegetable oil in a large pan over medium-high heat. - Add the diced chicken and cook until the chicken is cooked through, about 8 minutes. - In a separate bowl, beat the eggs and season with salt and pepper. - Reduce the heat to medium and add the eggs to the pan with the chicken. - Using a spatula, scramble the eggs until they are cooked through. - Add in the cooked rice and soy sauce and stir to combine with the chicken and eggs. - Cook the fried rice until everything is heated through, about 5 minutes. - Drizzle with sesame oil, season with more salt and pepper if desired, and serve. Enjoy! - """; - Recipe recipe = format.mapResponseToRecipe(mealType, responseText); - assertEquals(parsedResponse, recipe.toString()); - } - - @Test - public void testParseNoIndentsAndNewLines() { - String mealType = "LUNCH"; - String responseText = """ - Cheesy pasta bake - LUNCH - Ingredients: - - 1 lb pasta - - 1 lb ground beef - - 1 Tbsp olive oil - - 1 onion, diced - - 3 garlic cloves, minced - - 1 1/2 cans diced tomatoes - - 1 tsp oregano - - 1 tsp basil - - 1/2 tsp red pepper flakes - - Salt and pepper to taste - - 2-3 cups of shredded cheese - - Garlic bread - Instructions: - 1. Preheat oven to 375 degrees F. - 2. Cook pasta according to package instructions. Drain and set aside. - 3. Meanwhile, heat the olive oil in a large skillet over medium-high heat. - 4. Add the onion and garlic and cook until lightly browned and fragrant, about 3 minutes. - 5. Add the ground beef and cook until browned and crumbled, about 5-8 minutes. - 6. Add the tomatoes, oregano, basil, and red pepper flakes and season with salt and pepper to taste. Simmer the mixture for 8-10 minutes, or until the flavors have developed. - 7. In a greased 9x13 inch baking dish, combine the cooked pasta and beef mixture. - 8. Top with shredded cheese and bake in preheated oven for 25-30 minutes, or until the cheese is melted and bubbly. - 9. Serve the cheesy pasta bake with garlic bread. Enjoy! - """; - String parsedResponse = """ - Title: Cheesy pasta bake - Meal tag: LUNCH - Ingredients: - 1 lb pasta - 1 lb ground beef - 1 Tbsp olive oil - 1 onion, diced - 3 garlic cloves, minced - 1 1/2 cans diced tomatoes - 1 tsp oregano - 1 tsp basil - 1/2 tsp red pepper flakes - Salt and pepper to taste - 2-3 cups of shredded cheese - Garlic bread - Instructions: - Preheat oven to 375 degrees F. - Cook pasta according to package instructions. Drain and set aside. - Meanwhile, heat the olive oil in a large skillet over medium-high heat. - Add the onion and garlic and cook until lightly browned and fragrant, about 3 minutes. - Add the ground beef and cook until browned and crumbled, about 5-8 minutes. - Add the tomatoes, oregano, basil, and red pepper flakes and season with salt and pepper to taste. Simmer the mixture for 8-10 minutes, or until the flavors have developed. - In a greased 9x13 inch baking dish, combine the cooked pasta and beef mixture. - Top with shredded cheese and bake in preheated oven for 25-30 minutes, or until the cheese is melted and bubbly. - Serve the cheesy pasta bake with garlic bread. Enjoy! - """; - Recipe recipe = format.mapResponseToRecipe(mealType, responseText); - assertEquals(parsedResponse, recipe.toString()); - } - - @Test - public void testParseDifferentLineSpacing() { - String mealType = "DINNER"; - String responseText = """ - - Savory Beef Pasta Bake - DINNER - Ingredients: - - ½ pound of ground beef - - 1 box of your favorite pasta noodles - - 1 onion, chopped - - 4 cloves of garlic, minced - - 1 jar of your favorite marinara sauce - - 2 cups of shredded cheese (Mozzarella or Italian blend) - - 1 loaf of garlic bread - - Olive oil - - Salt and pepper to taste - - Instructions: - - 1. Preheat oven to 350°F. - - 2. Bring a large pot of salted water to a boil over high heat. - - 3. In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. - - 4. Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. - - 5. Add the marinara sauce and stir to combine. Simmer for 10 minutes. - - 6. Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. - - 7. Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. - - 8. Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. - - 9. Place the baking dish in the preheated oven and bake for 15 minutes. - - 10. Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. - - 11. Enjoy!"""; - String parsedResponse = """ - Title: Savory Beef Pasta Bake - Meal tag: DINNER - Ingredients: - ½ pound of ground beef - 1 box of your favorite pasta noodles - 1 onion, chopped - 4 cloves of garlic, minced - 1 jar of your favorite marinara sauce - 2 cups of shredded cheese (Mozzarella or Italian blend) - 1 loaf of garlic bread - Olive oil - Salt and pepper to taste - Instructions: - Preheat oven to 350°F. - Bring a large pot of salted water to a boil over high heat. - In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. - Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. - Add the marinara sauce and stir to combine. Simmer for 10 minutes. - Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. - Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. - Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. - Place the baking dish in the preheated oven and bake for 15 minutes. - Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. - Enjoy! - """; - Recipe recipe = format.mapResponseToRecipe(mealType, responseText); - assertEquals(parsedResponse, recipe.toString()); - } - - @Test - public void testParseRemoveDashesAndNumbers() { - String mealType = "LUNCH"; - String responseText = """ - - Savory Beef Pasta Bake - LUNCH - Ingredients: - - ½ pound of ground beef - - 1 box of your favorite pasta noodles - - 1 onion, chopped - - 4 cloves of garlic, minced - - 1 jar of your favorite marinara sauce - - 2 cups of shredded cheese (Mozzarella or Italian blend) - - 1 loaf of garlic bread - - Olive oil - - Salt and pepper to taste - - Instructions: - - 1. Preheat oven to 350°F. - - 2. Bring a large pot of salted water to a boil over high heat. - - 3. In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. - - 4. Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. - - 5. Add the marinara sauce and stir to combine. Simmer for 10 minutes. - - 6. Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. - - 7. Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. - - 8. Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. - - 9. Place the baking dish in the preheated oven and bake for 15 minutes. - - 10. Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. - - 11. Enjoy!"""; - String parsedResponse = """ - Title: Savory Beef Pasta Bake - Meal tag: LUNCH - Ingredients: - ½ pound of ground beef - 1 box of your favorite pasta noodles - 1 onion, chopped - 4 cloves of garlic, minced - 1 jar of your favorite marinara sauce - 2 cups of shredded cheese (Mozzarella or Italian blend) - 1 loaf of garlic bread - Olive oil - Salt and pepper to taste - Instructions: - Preheat oven to 350°F. - Bring a large pot of salted water to a boil over high heat. - In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. - Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. - Add the marinara sauce and stir to combine. Simmer for 10 minutes. - Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. - Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. - Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. - Place the baking dish in the preheated oven and bake for 15 minutes. - Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. - Enjoy! - """; - Recipe recipe = format.mapResponseToRecipe(mealType, responseText); - assertEquals(parsedResponse, recipe.toString()); - } + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + Model model = new Model(); + Format format = new Format(); + + @Test + /** + * Integration test for provide recipe + */ + public void testProvideRecipeIntegration() throws IOException, + URISyntaxException, InterruptedException { + // record and process audio + server.start(); + String mealType = "Breakfast"; // model.performWhisperRequest("GET", "mealtype"); + String ingredients = "Chicken, eggs."; // model.performWhisperRequest("GET2", "ingredients"); + + // build prompt for chatGPT + String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Breakfast. Chicken, eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; + String response = format.buildPrompt(mealType, ingredients); + assertEquals(prompt, response); + + // create Recipe object from response + String responseText = model.performChatGPTRequest("GET", mealType, ingredients); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, + responseText); + + chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); + + assertEquals("Fried Chicken", chatGPTrecipe.getTitle()); + assertEquals("Breakfast", chatGPTrecipe.getMealTag()); + assertNotNull(chatGPTrecipe.getImage()); + server.stop(); + } + + /** + * Unit tests for Recipe features + */ + @Test + public void testPromptBuild() { + TextToRecipe textToRecipe = new MockChatGPTRequestHandler(); + String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Lunch. I have rice, shrimp, chicken, and eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; + String response = textToRecipe.buildPrompt("Lunch", "I have rice, shrimp, chicken, and eggs."); + assertEquals(prompt, response); + } + + @Test + public void testRefreshRecipe() throws IOException, InterruptedException, + URISyntaxException { + server.stop(); + server.start(); + String mealType = "breakfast"; + String ingredients = "chicken, eggs"; + String initialResponse = model.performChatGPTRequest("GET", mealType, ingredients); + String expectedResponse = """ + Fried Chicken + breakfast + Ingredients: + - 2 chicken breasts, diced + - 2 eggs + Instructions: + 1. Crack 2 eggs into bowl. + 2. Add chicken into bowl and then fry. + 3. Enjoy! + """; + assertEquals(expectedResponse, initialResponse); + + // simulate refresh + String refreshResponse = model.performChatGPTRequest("GET2", mealType, ingredients); + assertNotEquals(initialResponse, refreshResponse); + server.stop(); + } + + @Test + public void testParseJSON() { + String mealType = "BREAKFAST"; + String responseText = """ + Fried Chicken and Egg Fried Rice + BREAKFAST + Ingredients: + + - 2 chicken breasts, diced + - 2 large eggs + - 2 cups cooked rice + - 2 tablespoons vegetable oil + - 2 tablespoons soy sauce + - 1 teaspoon sesame oil + - Salt and pepper to taste + + Instructions: + + 1. Heat the vegetable oil in a large pan over medium-high heat. + + 2. Add the diced chicken and cook until the chicken is cooked through, about 8 minutes. + + 3. In a separate bowl, beat the eggs and season with salt and pepper. + + 4. Reduce the heat to medium and add the eggs to the pan with the chicken. + + 5. Using a spatula, scramble the eggs until they are cooked through. + + 6. Add in the cooked rice and soy sauce and stir to combine with the chicken and eggs. + + 7. Cook the fried rice until everything is heated through, about 5 minutes. + + 8. Drizzle with sesame oil, season with more salt and pepper if desired, and serve. Enjoy! + """; + String parsedResponse = """ + Title: Fried Chicken and Egg Fried Rice + Meal tag: BREAKFAST + Ingredients: + 2 chicken breasts, diced + 2 large eggs + 2 cups cooked rice + 2 tablespoons vegetable oil + 2 tablespoons soy sauce + 1 teaspoon sesame oil + Salt and pepper to taste + Instructions: + Heat the vegetable oil in a large pan over medium-high heat. + Add the diced chicken and cook until the chicken is cooked through, about 8 minutes. + In a separate bowl, beat the eggs and season with salt and pepper. + Reduce the heat to medium and add the eggs to the pan with the chicken. + Using a spatula, scramble the eggs until they are cooked through. + Add in the cooked rice and soy sauce and stir to combine with the chicken and eggs. + Cook the fried rice until everything is heated through, about 5 minutes. + Drizzle with sesame oil, season with more salt and pepper if desired, and serve. Enjoy! + """; + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); + assertEquals(parsedResponse, recipe.toString()); + } + + @Test + public void testParseNoIndentsAndNewLines() { + String mealType = "LUNCH"; + String responseText = """ + Cheesy pasta bake + LUNCH + Ingredients: + - 1 lb pasta + - 1 lb ground beef + - 1 Tbsp olive oil + - 1 onion, diced + - 3 garlic cloves, minced + - 1 1/2 cans diced tomatoes + - 1 tsp oregano + - 1 tsp basil + - 1/2 tsp red pepper flakes + - Salt and pepper to taste + - 2-3 cups of shredded cheese + - Garlic bread + Instructions: + 1. Preheat oven to 375 degrees F. + 2. Cook pasta according to package instructions. Drain and set aside. + 3. Meanwhile, heat the olive oil in a large skillet over medium-high heat. + 4. Add the onion and garlic and cook until lightly browned and fragrant, about 3 minutes. + 5. Add the ground beef and cook until browned and crumbled, about 5-8 minutes. + 6. Add the tomatoes, oregano, basil, and red pepper flakes and season with salt and pepper to taste. Simmer the mixture for 8-10 minutes, or until the flavors have developed. + 7. In a greased 9x13 inch baking dish, combine the cooked pasta and beef mixture. + 8. Top with shredded cheese and bake in preheated oven for 25-30 minutes, or until the cheese is melted and bubbly. + 9. Serve the cheesy pasta bake with garlic bread. Enjoy! + """; + String parsedResponse = """ + Title: Cheesy pasta bake + Meal tag: LUNCH + Ingredients: + 1 lb pasta + 1 lb ground beef + 1 Tbsp olive oil + 1 onion, diced + 3 garlic cloves, minced + 1 1/2 cans diced tomatoes + 1 tsp oregano + 1 tsp basil + 1/2 tsp red pepper flakes + Salt and pepper to taste + 2-3 cups of shredded cheese + Garlic bread + Instructions: + Preheat oven to 375 degrees F. + Cook pasta according to package instructions. Drain and set aside. + Meanwhile, heat the olive oil in a large skillet over medium-high heat. + Add the onion and garlic and cook until lightly browned and fragrant, about 3 minutes. + Add the ground beef and cook until browned and crumbled, about 5-8 minutes. + Add the tomatoes, oregano, basil, and red pepper flakes and season with salt and pepper to taste. Simmer the mixture for 8-10 minutes, or until the flavors have developed. + In a greased 9x13 inch baking dish, combine the cooked pasta and beef mixture. + Top with shredded cheese and bake in preheated oven for 25-30 minutes, or until the cheese is melted and bubbly. + Serve the cheesy pasta bake with garlic bread. Enjoy! + """; + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); + assertEquals(parsedResponse, recipe.toString()); + } + + @Test + public void testParseDifferentLineSpacing() { + String mealType = "DINNER"; + String responseText = """ + + Savory Beef Pasta Bake + DINNER + Ingredients: + - ½ pound of ground beef + - 1 box of your favorite pasta noodles + - 1 onion, chopped + - 4 cloves of garlic, minced + - 1 jar of your favorite marinara sauce + - 2 cups of shredded cheese (Mozzarella or Italian blend) + - 1 loaf of garlic bread + - Olive oil + - Salt and pepper to taste + + Instructions: + + 1. Preheat oven to 350°F. + + 2. Bring a large pot of salted water to a boil over high heat. + + 3. In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. + + 4. Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. + + 5. Add the marinara sauce and stir to combine. Simmer for 10 minutes. + + 6. Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. + + 7. Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. + + 8. Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. + + 9. Place the baking dish in the preheated oven and bake for 15 minutes. + + 10. Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. + + 11. Enjoy!"""; + String parsedResponse = """ + Title: Savory Beef Pasta Bake + Meal tag: DINNER + Ingredients: + ½ pound of ground beef + 1 box of your favorite pasta noodles + 1 onion, chopped + 4 cloves of garlic, minced + 1 jar of your favorite marinara sauce + 2 cups of shredded cheese (Mozzarella or Italian blend) + 1 loaf of garlic bread + Olive oil + Salt and pepper to taste + Instructions: + Preheat oven to 350°F. + Bring a large pot of salted water to a boil over high heat. + In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. + Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. + Add the marinara sauce and stir to combine. Simmer for 10 minutes. + Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. + Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. + Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. + Place the baking dish in the preheated oven and bake for 15 minutes. + Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. + Enjoy! + """; + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); + assertEquals(parsedResponse, recipe.toString()); + } + + @Test + public void testParseRemoveDashesAndNumbers() { + String mealType = "LUNCH"; + String responseText = """ + + Savory Beef Pasta Bake + LUNCH + Ingredients: + - ½ pound of ground beef + - 1 box of your favorite pasta noodles + - 1 onion, chopped + - 4 cloves of garlic, minced + - 1 jar of your favorite marinara sauce + - 2 cups of shredded cheese (Mozzarella or Italian blend) + - 1 loaf of garlic bread + - Olive oil + - Salt and pepper to taste + + Instructions: + + 1. Preheat oven to 350°F. + + 2. Bring a large pot of salted water to a boil over high heat. + + 3. In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. + + 4. Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. + + 5. Add the marinara sauce and stir to combine. Simmer for 10 minutes. + + 6. Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. + + 7. Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. + + 8. Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. + + 9. Place the baking dish in the preheated oven and bake for 15 minutes. + + 10. Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. + + 11. Enjoy!"""; + String parsedResponse = """ + Title: Savory Beef Pasta Bake + Meal tag: LUNCH + Ingredients: + ½ pound of ground beef + 1 box of your favorite pasta noodles + 1 onion, chopped + 4 cloves of garlic, minced + 1 jar of your favorite marinara sauce + 2 cups of shredded cheese (Mozzarella or Italian blend) + 1 loaf of garlic bread + Olive oil + Salt and pepper to taste + Instructions: + Preheat oven to 350°F. + Bring a large pot of salted water to a boil over high heat. + In a large skillet, heat the olive oil over medium-high heat. Add the chopped onion and minced garlic and sauté until the onion is soft and fragrant, about 3 minutes. + Add the ground beef and cook until the beef is no longer pink, about 5 minutes, stirring frequently. + Add the marinara sauce and stir to combine. Simmer for 10 minutes. + Meanwhile, add the pasta to the pot of boiling water and cook for 6 minutes or according to the package directions. + Drain the cooked pasta and add it to the skillet with the beef and sauce. Stir to combine. + Transfer the pasta and beef mixture to a 9?x13? baking dish. Sprinkle the top with the shredded cheese. + Place the baking dish in the preheated oven and bake for 15 minutes. + Remove from oven and discard the garlic bread or serve alongside the beef pasta bake. + Enjoy! + """; + Recipe recipe = format.mapResponseToRecipe(mealType, responseText); + assertEquals(parsedResponse, recipe.toString()); + } } From 00562b530771bb3cda8808bd06b561570ee2b748 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Mon, 4 Dec 2023 19:48:43 -0800 Subject: [PATCH 09/31] ci: mock whisper requests --- .../server/MockWhisperRequestHandler.java | 2 +- app/src/test/java/code/RefreshTest.java | 126 +++++++++++------- app/src/test/java/code/TextToRecipeTest.java | 27 ---- app/src/test/java/code/VoiceToTextTest.java | 10 +- 4 files changed, 85 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/code/server/MockWhisperRequestHandler.java b/app/src/main/java/code/server/MockWhisperRequestHandler.java index f19c8eb..f9771a0 100644 --- a/app/src/main/java/code/server/MockWhisperRequestHandler.java +++ b/app/src/main/java/code/server/MockWhisperRequestHandler.java @@ -15,7 +15,7 @@ public MockWhisperRequestHandler(IHttpConnection connection) { } public String processAudio(String type) throws IOException, URISyntaxException { - if (type.equals("mealtype")) { + if (type.equals("mealType")) { // processed correctly return "Breakfast"; } else if (type.equals("ingredients")) { diff --git a/app/src/test/java/code/RefreshTest.java b/app/src/test/java/code/RefreshTest.java index 306cf7f..ca78dbd 100644 --- a/app/src/test/java/code/RefreshTest.java +++ b/app/src/test/java/code/RefreshTest.java @@ -1,48 +1,78 @@ -// package code; - -// import code.client.Controllers.Controller; -// import code.client.Model.MockGPTService; -// import code.client.Model.MockDallEService; -// import code.client.Model.Model; -// import code.client.View.DetailsAppFrame; -// import code.client.View.View; -// import javafx.scene.control.Button; -// import javafx.stage.Stage; -// import org.junit.jupiter.api.Test; -// import code.client.Model.Account; -// import code.client.Model.AccountCSVReader; -// import code.client.Model.AccountCSVWriter; -// import code.client.Model.ChatGPTService; -// import code.client.View.*; -// import code.client.Controllers.*; -// import code.server.*; - -// import java.io.IOException; -// import java.net.URISyntaxException; -// import java.util.*; - -// public class RefreshTest { -// @Test -// public void refreshTesting() { -// DetailsAppFrame detailsAppFrame = new DetailsAppFrame(); -// // TextToRecipe txt = new TextToRecipe(); -// // initial recipe before refresh -// Recipe initialRecipe = detailsAppFrame.getDisplayedRecipe(); - -// MockGPTService mockGPT = new MockGPTService(); -// Recipe refreshedRecipe; -// } - -// private Recipe generateRefresh(Recipe originalRecipe, MockGPTService -// mockGPTService) -// throws IOException, InterruptedException, URISyntaxException { -// String mealType = "Breakfast"; -// String ingredients = "Chicken, eggs."; - -// String refreshedResponse = mockGPTService.getResponse(mealType, ingredients); -// Recipe out = mockGPTService.mapResponseToRecipe(mealType, refreshedResponse); - -// return out; -// } - -// } \ No newline at end of file +package code; + +import code.client.Controllers.Controller; +import code.client.Model.Model; +import code.client.View.DetailsAppFrame; +import code.client.View.View; +import javafx.scene.control.Button; +import javafx.stage.Stage; +import org.junit.jupiter.api.Test; +import code.client.Model.Account; +import code.client.Model.AccountCSVReader; +import code.client.Model.AccountCSVWriter; +import code.client.Model.AppConfig; +import code.client.View.*; +import code.client.Controllers.*; +import code.server.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.*; + +public class RefreshTest { + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + Model model = new Model(); + + @Test + public void testRefreshRecipe() throws IOException, InterruptedException, + URISyntaxException { + server.start(); + String mealType = "breakfast"; + String ingredients = "chicken, eggs"; + String initialResponse = model.performChatGPTRequest("GET", mealType, ingredients); + String expectedResponse = """ + Fried Chicken + breakfast + Ingredients: + - 2 chicken breasts, diced + - 2 eggs + Instructions: + 1. Crack 2 eggs into bowl. + 2. Add chicken into bowl and then fry. + 3. Enjoy! + """; + assertEquals(expectedResponse, initialResponse); + + // simulate refresh + String refreshResponse = model.performChatGPTRequest("GET2", mealType, ingredients); + assertNotEquals(initialResponse, refreshResponse); + server.stop(); + } + + // @Test + // public void refreshTesting() { + // DetailsAppFrame detailsAppFrame = new DetailsAppFrame(); + // // TextToRecipe txt = new TextToRecipe(); + // // initial recipe before refresh + // Recipe initialRecipe = detailsAppFrame.getDisplayedRecipe(); + + // MockGPTService mockGPT = new MockGPTService(); + // Recipe refreshedRecipe; + // } + + // private Recipe generateRefresh(Recipe originalRecipe, MockGPTService + // mockGPTService) + // throws IOException, InterruptedException, URISyntaxException { + // String mealType = "Breakfast"; + // String ingredients = "Chicken, eggs."; + + // String refreshedResponse = mockGPTService.getResponse(mealType, ingredients); + // Recipe out = mockGPTService.mapResponseToRecipe(mealType, refreshedResponse); + + // return out; + // } + +} \ No newline at end of file diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index f3a8abd..a112fcc 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -59,33 +59,6 @@ public void testPromptBuild() { assertEquals(prompt, response); } - @Test - public void testRefreshRecipe() throws IOException, InterruptedException, - URISyntaxException { - server.stop(); - server.start(); - String mealType = "breakfast"; - String ingredients = "chicken, eggs"; - String initialResponse = model.performChatGPTRequest("GET", mealType, ingredients); - String expectedResponse = """ - Fried Chicken - breakfast - Ingredients: - - 2 chicken breasts, diced - - 2 eggs - Instructions: - 1. Crack 2 eggs into bowl. - 2. Add chicken into bowl and then fry. - 3. Enjoy! - """; - assertEquals(expectedResponse, initialResponse); - - // simulate refresh - String refreshResponse = model.performChatGPTRequest("GET2", mealType, ingredients); - assertNotEquals(initialResponse, refreshResponse); - server.stop(); - } - @Test public void testParseJSON() { String mealType = "BREAKFAST"; diff --git a/app/src/test/java/code/VoiceToTextTest.java b/app/src/test/java/code/VoiceToTextTest.java index ba8005d..538d802 100644 --- a/app/src/test/java/code/VoiceToTextTest.java +++ b/app/src/test/java/code/VoiceToTextTest.java @@ -11,6 +11,9 @@ import code.server.IHttpConnection; import code.server.MockHttpConnection; import code.server.MockServer; +import code.server.MockWhisperRequestHandler; +import code.server.VoiceToText; + import static org.junit.jupiter.api.Assertions.assertEquals; public class VoiceToTextTest { @@ -22,13 +25,12 @@ public class VoiceToTextTest { */ @Test void testSuccessfulProcessAudio() throws IOException, URISyntaxException { - server.start(); - String response = model.performWhisperRequest("GET", "mealType"); + VoiceToText voiceToText = new MockWhisperRequestHandler(); + String response = voiceToText.processAudio("mealType"); assertEquals("Breakfast", response); - response = model.performWhisperRequest("GET2", "ingredients"); + response = voiceToText.processAudio("ingredients"); assertEquals("Chicken, eggs.", response); - } /* From 9ade854d67a092e6ea2245fc1d3bfd2a51673684 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Mon, 4 Dec 2023 20:14:29 -0800 Subject: [PATCH 10/31] fix: go to offlineUI if user request fails --- app/src/main/java/code/client/Controllers/Controller.java | 8 +++++++- app/src/main/java/code/server/AccountRequestHandler.java | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index bc7727d..a1344bf 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -499,6 +499,9 @@ private boolean isUsernameTaken(String username, String password) { // Check if the username is already taken // temporary logic, no database yet String response = model.performAccountRequest("GET", username, password); + if (response.contains("Offline")) { + view.goToOfflineUI(); + } // System.out.println("Response for usernameTaken : " + response); return (response.equals("Username is taken")); } @@ -593,7 +596,10 @@ private void showLoginSuccessPane(GridPane grid, boolean loginSuccessful) { private boolean performLogin(String username, String password) { // Will add logic for failed login later String response = model.performAccountRequest("GET", username, password); - if (response.equals(AccountRequestHandler.USERNAME_NOT_FOUND) || + if (response.contains("Offline")) { + view.goToOfflineUI(); + return false; + } else if (response.equals(AccountRequestHandler.USERNAME_NOT_FOUND) || response.equals(AccountRequestHandler.INCORRECT_PASSWORD) || response.equals(AccountRequestHandler.TAKEN_USERNAME)) { return false; diff --git a/app/src/main/java/code/server/AccountRequestHandler.java b/app/src/main/java/code/server/AccountRequestHandler.java index b388aa9..e8d2b84 100644 --- a/app/src/main/java/code/server/AccountRequestHandler.java +++ b/app/src/main/java/code/server/AccountRequestHandler.java @@ -1,5 +1,6 @@ package code.server; +import com.mongodb.MongoTimeoutException; import com.sun.net.httpserver.*; import java.io.*; @@ -29,6 +30,8 @@ public void handle(HttpExchange httpExchange) throws IOException { } else { throw new Exception("Not valid request method."); } + } catch (MongoTimeoutException e) { + response = "Server Offline"; } catch (Exception e) { System.out.println("An erroneous request"); e.printStackTrace(); From f2e29cae94eab3c641157fd5cedfba1186ceddb4 Mon Sep 17 00:00:00 2001 From: AllKeng Date: Mon, 4 Dec 2023 21:49:16 -0800 Subject: [PATCH 11/31] Fixed WhisperAPI responses, fixed upserting. Co-authored-by: dashluu --- .../code/client/Controllers/Controller.java | 7 ++- .../main/java/code/client/Model/Model.java | 17 ++++-- app/src/main/java/code/server/AppServer.java | 6 +- .../main/java/code/server/RecipeMongoDb.java | 40 +++++++----- .../main/java/code/server/WHISPERSERVER.java | 0 .../code/server/WhisperRequestHandler.java | 57 +---------------- .../main/java/code/server/WhisperService.java | 61 +++++++++++++++++++ 7 files changed, 107 insertions(+), 81 deletions(-) delete mode 100644 app/src/main/java/code/server/WHISPERSERVER.java create mode 100644 app/src/main/java/code/server/WhisperService.java diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index a1344bf..ecc1c4f 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -105,7 +105,7 @@ private void handleRecipePostButton(ActionEvent event) throws IOException { Button saveButtonFromDetailed = view.getDetailedView().getSaveButton(); saveButtonFromDetailed.setStyle(blinkStyle); - PauseTransition pause = new PauseTransition(Duration.seconds(1)); + PauseTransition pause = new PauseTransition(Duration.seconds(2.5)); pause.setOnFinished(f -> saveButtonFromDetailed.setStyle(defaultButtonStyle)); pause.play(); @@ -360,7 +360,8 @@ private void deleteGivenRecipe(Recipe recipe) throws IOException { System.out.println("Deleting id: " + recipe.getId()); model.performRecipeRequest("DELETE", recipeStr, null); - this.view.getAppFrameHome().updateDisplay(filter); + this.view.getAppFrameHome().updateDisplay(filter); + goToRecipeList(); } private void handleShareButton(ActionEvent event) { @@ -664,7 +665,7 @@ private void recordIngredients() { // recordingLabel2.setStyle(""); try { - mealType = model.performWhisperRequest("GET", "ingredients"); + ingredients = model.performWhisperRequest("GET", "ingredients"); String nonAsciiCharactersRegex = "[^\\x00-\\x7F]"; if (ingredients.matches(".*" + nonAsciiCharactersRegex + ".*") || diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index bab91ea..4906c77 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -153,9 +153,17 @@ public String performWhisperRequest(String method, String type) throws Malformed Files.copy(audioFile.toPath(), output); output.flush(); int responseCode = ((HttpURLConnection) connection).getResponseCode(); - response = ((HttpURLConnection) connection).getResponseMessage(); System.out.println("Response code: [" + responseCode + "]"); - + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line; + response = ""; + + while ((line = in.readLine()) != null) { + response += line + "\n"; + } + + in.close(); + if (type.equals("mealType") && responseCode == 200) { response = response.toUpperCase(); if (response.contains("BREAKFAST")) { @@ -165,11 +173,12 @@ public String performWhisperRequest(String method, String type) throws Malformed } else if (response.contains("DINNER")) { response = "Dinner"; } else { - response = null; + response = "NONE OF THE ABOVE"; } } } - + + System.out.println("Whisper response: " + response); return response; } } \ No newline at end of file diff --git a/app/src/main/java/code/server/AppServer.java b/app/src/main/java/code/server/AppServer.java index 0b55f24..ab94b0f 100644 --- a/app/src/main/java/code/server/AppServer.java +++ b/app/src/main/java/code/server/AppServer.java @@ -47,11 +47,7 @@ public void start() throws IOException { httpServer.createContext(AppConfig.SHARE_PATH, new ShareRequestHandler(accountMongoDB, recipeDb)); httpServer.createContext(AppConfig.CHATGPT_PATH, new ChatGPTRequestHandler()); httpServer.createContext(AppConfig.DALLE_PATH, new DallERequestHandler()); - try { - httpServer.createContext(AppConfig.WHISPER_PATH, new WhisperRequestHandler()); - } catch (URISyntaxException e) { - e.printStackTrace(); - } + httpServer.createContext(AppConfig.WHISPER_PATH, new WhisperRequestHandler()); // set the executor httpServer.setExecutor(threadPoolExecutor); // start the server diff --git a/app/src/main/java/code/server/RecipeMongoDb.java b/app/src/main/java/code/server/RecipeMongoDb.java index 764a820..fac7915 100644 --- a/app/src/main/java/code/server/RecipeMongoDb.java +++ b/app/src/main/java/code/server/RecipeMongoDb.java @@ -9,8 +9,14 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.Updates; +import java.util.Arrays; import static com.mongodb.client.model.Filters.eq; +import static com.mongodb.client.model.Updates.*; import java.util.List; import java.util.ArrayList; @@ -66,15 +72,24 @@ public List getList(String accountId) { @Override public boolean add(Recipe recipe) { - Document recipeDocument = new Document("_id", new ObjectId(recipe.getId())); - recipeDocument.append("userID", recipe.getAccountId()) - .append("title", recipe.getTitle()) - .append("mealTag", recipe.getMealTag()) - .append("ingredients", Lists.newArrayList(recipe.getIngredientIterator())) - .append("instructions", Lists.newArrayList(recipe.getInstructionIterator())) - .append("date",recipe.getDate()) - .append("image", recipe.getImage()); - recipeDocumentCollection.insertOne(recipeDocument); + Bson filter = eq("_id", new ObjectId(recipe.getId())); + List updates = new ArrayList<>(); + Bson updateUserId = set("userID", recipe.getAccountId()); + Bson updateTitle = set("title", recipe.getTitle()); + Bson updateMealTag = set("mealTag", recipe.getMealTag()); + Bson updateIngr = set("ingredients", Lists.newArrayList(recipe.getIngredientIterator())); + Bson updateInstr = set("instructions", Lists.newArrayList(recipe.getInstructionIterator())); + Bson updateDate = set("date", recipe.getDate()); + Bson updateImage = set("image", recipe.getImage()); + updates.addAll(Arrays.asList(updateUserId, + updateTitle, + updateMealTag, + updateIngr, + updateInstr, + updateDate, + updateImage)); + UpdateOptions options = new UpdateOptions().upsert(true); + recipeDocumentCollection.updateOne(filter, updates, options); return true; } @@ -92,12 +107,7 @@ public Recipe find(String id) { @Override public boolean update(Recipe updatedRecipe) { - Recipe oldRecipe = remove(updatedRecipe.getId()); - if (oldRecipe == null) { - // Recipe does not exist - return false; - } - add(updatedRecipe); + Bson filter = eq("_id", updatedRecipe.getId()); return true; } diff --git a/app/src/main/java/code/server/WHISPERSERVER.java b/app/src/main/java/code/server/WHISPERSERVER.java deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/code/server/WhisperRequestHandler.java b/app/src/main/java/code/server/WhisperRequestHandler.java index 00bec3c..ab219b7 100644 --- a/app/src/main/java/code/server/WhisperRequestHandler.java +++ b/app/src/main/java/code/server/WhisperRequestHandler.java @@ -5,15 +5,8 @@ import code.client.Model.AppConfig; import java.io.*; -import java.net.*; -public class WhisperRequestHandler extends VoiceToText implements HttpHandler { - public static final String API_ENDPOINT = "https://api.openai.com/v1/audio/transcriptions"; - public static final String MODEL = "whisper-1"; - - public WhisperRequestHandler() throws URISyntaxException, IOException { - super(new AppHttpConnection(API_ENDPOINT)); - } +public class WhisperRequestHandler implements HttpHandler { @Override public void handle(HttpExchange httpExchange) throws IOException { @@ -53,6 +46,8 @@ public void handle(HttpExchange httpExchange) throws IOException { audioOutStream.write(audioByteArray, 0, audioFileSize); audioOutStream.flush(); audioOutStream.close(); + VoiceToText whisperService = new WhisperService(); + response = whisperService.processAudio(""); } catch (Exception e) { System.out.println("An erroneous request"); e.printStackTrace(); @@ -93,50 +88,4 @@ private static String readLine(InputStream multipartInStream, String lineSeparat throw new IOException("Reached end of stream while reading the current line!"); } - - // https://stackoverflow.com/questions/25334139/how-to-mock-a-url-connection - public String processAudio(String type) throws IOException, URISyntaxException { - // Send HTTP request - sendHttpRequest(); - return super.processAudio(type); - } - - private void sendHttpRequest() throws IOException, URISyntaxException { - // Set up request headers - File file = new File(AppConfig.AUDIO_FILE); - String boundary = "Boundary-" + System.currentTimeMillis(); - connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); - connection.setRequestProperty("Authorization", "Bearer " + AppConfig.API_KEY); - connection.setRequestMethod("POST"); - connection.setDoOutput(true); - - // Set up output stream to write request body - OutputStream outputStream = connection.getOutputStream(); - // Write model parameter to request body - outputStream.write(("--" + boundary + "\r\n").getBytes()); - outputStream.write( - ("Content-Disposition: form-data; name=\"model\"\r\n\r\n").getBytes()); - outputStream.write((MODEL + "\r\n").getBytes()); - // Write file parameter to request body - outputStream.write(("--" + boundary + "\r\n").getBytes()); - outputStream.write(("Content-Disposition: form-data; name=\"file\"; filename=\"" + - file.getName() + - "\"\r\n").getBytes()); - outputStream.write(("Content-Type: audio/mpeg\r\n\r\n").getBytes()); - - FileInputStream fileInputStream = new FileInputStream(file); - byte[] buffer = new byte[1024]; - int bytesRead; - - while ((bytesRead = fileInputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - - fileInputStream.close(); - // Write closing boundary to request body - outputStream.write(("\r\n--" + boundary + "--\r\n").getBytes()); - // Flush and close output stream - outputStream.flush(); - outputStream.close(); - } } \ No newline at end of file diff --git a/app/src/main/java/code/server/WhisperService.java b/app/src/main/java/code/server/WhisperService.java new file mode 100644 index 0000000..15a7d16 --- /dev/null +++ b/app/src/main/java/code/server/WhisperService.java @@ -0,0 +1,61 @@ +package code.server; + +import code.client.Model.AppConfig; + +import java.io.*; +import java.net.*; + +public class WhisperService extends VoiceToText { + public static final String API_ENDPOINT = "https://api.openai.com/v1/audio/transcriptions"; + public static final String MODEL = "whisper-1"; + + public WhisperService() throws URISyntaxException, IOException { + super(new AppHttpConnection(API_ENDPOINT)); + } + + // https://stackoverflow.com/questions/25334139/how-to-mock-a-url-connection + public String processAudio(String type) throws IOException, URISyntaxException { + // Send HTTP request + sendHttpRequest(); + return super.processAudio(type); + } + + private void sendHttpRequest() throws IOException, URISyntaxException { + // Set up request headers + File file = new File(AppConfig.AUDIO_FILE); + String boundary = "Boundary-" + System.currentTimeMillis(); + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + connection.setRequestProperty("Authorization", "Bearer " + AppConfig.API_KEY); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + + // Set up output stream to write request body + OutputStream outputStream = connection.getOutputStream(); + // Write model parameter to request body + outputStream.write(("--" + boundary + "\r\n").getBytes()); + outputStream.write( + ("Content-Disposition: form-data; name=\"model\"\r\n\r\n").getBytes()); + outputStream.write((MODEL + "\r\n").getBytes()); + // Write file parameter to request body + outputStream.write(("--" + boundary + "\r\n").getBytes()); + outputStream.write(("Content-Disposition: form-data; name=\"file\"; filename=\"" + + file.getName() + + "\"\r\n").getBytes()); + outputStream.write(("Content-Type: audio/mpeg\r\n\r\n").getBytes()); + + FileInputStream fileInputStream = new FileInputStream(file); + byte[] buffer = new byte[1024]; + int bytesRead; + + while ((bytesRead = fileInputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + + fileInputStream.close(); + // Write closing boundary to request body + outputStream.write(("\r\n--" + boundary + "--\r\n").getBytes()); + // Flush and close output stream + outputStream.flush(); + outputStream.close(); + } +} \ No newline at end of file From 5c9128a60ba3237c240f06619deb7257eda83e81 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski <33042752+sprestrelski@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:49:29 -0800 Subject: [PATCH 12/31] chore: Delete app/bin/main/code/client/View directory --- app/bin/main/code/client/View/microphone.png | Bin 2058 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/bin/main/code/client/View/microphone.png diff --git a/app/bin/main/code/client/View/microphone.png b/app/bin/main/code/client/View/microphone.png deleted file mode 100644 index 3167b192521c0d91189fda7137f882c12c830051..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2058 zcma)-`9Bkk1II}{j&eL>ERQUra^w6}J)?eoYMornsb#r^cPE_Tmj zCZ_$m&_dImM-=_mThW*neUSQ<)-}yhpV&#Ag`?0f-SpG?PuAuLuRTq4#t8*x1-R9i z%vD6jacN$=;Au~sSMPNDYP=ruHV!K|7c0)%okrGsjTwqb?detq0>O;9=F*?I_P9E3 zy==)w=S^HE_g=b`Kd5Z2bQc{Y8t6ovIr$*D#N0DA2`(bAuylPVI?IgV_L;8yJKMye z*z5IRE<1md;ZOJ=5U5urc?^KkeO?!H18T*=bf1R$c}131E}hVj73N49l>U%B2ru@! ze9+)u_=k#-JN>N|`P$tinn6@+sB`2~jby^`1!)r%m(&y|`TTsEe%*}ZIz@`UpjQ`l z?E@Gvo+!(fgM(=@fZ+llzF&H@c)F6Ugp^>;`^Dt91=i>&4xJFSXjEpCS|MW>e>*Hv zE22qno3^NZ>C1)I%~iA2k$_VhAv6!O!`bQ(1th?d7qLT=01li0mO`8_gPk9VIF!WN zE2u&VTjqoxK=A(oA#xLgfjB~%+K}q7`p3>c-NwX8%nBBw5MLdsd+J=kHZk4@h7la} zbMZHAd;A-uso8K$n}H0sTXOaX&NCoTjo$*PpM)LUrsCC6!3}qJJ&@Uxrq3be__vJ1 z+|%q>No*a-LBVKQi>GiZFRtw@uq);UFGZ7WDACQG?s1=4-|wxR^IVdF6*5kbgYA}%57D56h&2fIRC>PB9kzU6pP2xo zM99emBmqayuDuL7e^iA!P5Bvg<%CYOT|{6-J<8M8tagtdA^hacyWPsLzGI}e$;l9D z%X{riU5`vRbEbc{oLF)al0zB}+p*9Ci(8BlSi7`GKZz=#4sxMyaS-P{4wPcSzH%7&! zEDs@yTIwB=!?|$33bIszR~i+;p%zuVB|osgMjfjPkBqb^f;}siCBo2711*(^nK^A< z^^ISy+5lECH}6IxIT3VP$Mt#g4t1A|LA6eZ;mx*l?Z$AO~T$)4Rzd5 zO}ZT{)CRySf2{etMAm+N$jU+|f)#{G5+0LnN|Z#gs^OHe+wb=trdo{tBLZOuoNaXP zxZWh)+_>N|L1mx+(vjBw_A^4*DDeDhKlLK3d?RaFH3*7kz%Wj)UVs#8X~wedzsoD1 z9qLM(##hA~9%4cIo>#ZnFgTgT7X1lB{#v`~D|*iyjaTd4QwyjBC$?vuNjcg;4UG4h zhUDY`O36a}w#RvKPKxJdVk=jsE-|Yc$&IUG?al8@WScLhE--fmw$^sne4Y0a(M9)h zHnX9UO=cBQpKSpnQQEG)CbTPPA0lr1BS-OhX#%r0CwJ9*pbs7zq+aGWPIk%OC+F@; zCW4rV&}5~LS0sDBF>Ze0Dl)~*-E%t?>|`RYdv%B8TG4_Y!iYf0N#w*+A1*v+4>x$& zjXwIT`J0~by8`jfHts{wua&!DkR~K4_2)jyaDXU-Cu)8u5I=Mbu^kMwzI#uI>gu@-oJ{=8)$VT z!>eRkH3ttdz^hhg3=wgK13MHADUc=qxm%qNXQLw+>N`f7Ly%p(#*HFyZ}&5De_5sC zhoGm{KI>tzf|V?gQ}1kJ+DrjmU?m5sM~Wi91BD`vNN#^6;RD!FUQW_((DO=|r? zg7@LalKN4mRZ*X@F#h4rpye90GA?VUXG+vG zDUx+ByUTPIi?mI5Ob<>g!5+{fuC6)~=i;<{C`QXEruadYN5+ir;}oL2F?L+z1<<%_ zeyYZzWB8)%HR{Er0B+CGm1;V1GJBDKrs(8EP>pbJWu$+9bUwJHEwmbfO8ggmya?_9 From 5713cee00eaeeda1162d680ac4fc4a452fc9b05d Mon Sep 17 00:00:00 2001 From: AllKeng Date: Mon, 4 Dec 2023 23:29:23 -0800 Subject: [PATCH 13/31] Loading Screen + Threads :) Co-authored-by: dashluu Co-authored-by: Samantha Prestrelski --- .../code/client/Controllers/Controller.java | 96 ++++++++++++++---- .../main/java/code/client/View/LoadingUI.java | 35 +++++++ app/src/main/java/code/client/View/View.java | 6 ++ .../main/java/code/client/View/loading.png | Bin 0 -> 134091 bytes 4 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/code/client/View/LoadingUI.java create mode 100644 app/src/main/java/code/client/View/loading.png diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index ecc1c4f..f46e8c8 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -60,6 +60,7 @@ public class Controller { private final AppAudioRecorder recorder = new AppAudioRecorder(); private String mealType; // stores the meal type specified by the user private String ingredients; // stores the ingredients listed out by the user + private ProgressBar progressBar; public Controller(View view, Model model) { @@ -99,6 +100,7 @@ public void setTitle(String title) { } private void handleRecipePostButton(ActionEvent event) throws IOException { + view.getDetailedView().getRefreshButton().setVisible(false); Recipe postedRecipe = view.getDetailedView().getDisplayedRecipe(); Date currTime = new Date(); postedRecipe.setDate(currTime.getTime()); @@ -117,7 +119,12 @@ private void handleRecipePostButton(ActionEvent event) throws IOException { // Debugging // System.out.println("Posting: " + recipe); - model.performRecipeRequest("POST", recipe, null); + String response = model.performRecipeRequest("POST", recipe, null); + if (response.contains("Offline")) { + + } else if (response.contains("Error")) { + + } } private void goToRecipeList() { @@ -292,17 +299,30 @@ public void addListenersToList() { private void handleDetailedViewFromNewRecipeButton(ActionEvent event) { // Get ChatGPT response from the Model if (mealType != null && ingredients != null) { + view.goToLoading(); try { - String responseText = model.performChatGPTRequest("GET", mealType, ingredients); - Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); - chatGPTrecipe.setAccountId(account.getId()); - chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); - - // Changes UI to Detailed Recipe Screen - view.goToDetailedView(chatGPTrecipe, false); - view.getDetailedView().getRecipeDetailsUI().setEditable(false); - handleDetailedViewListeners(); - + Thread thread = new Thread( + new Runnable() { + @Override + public void run() { + String responseText = model.performChatGPTRequest("GET", mealType, ingredients); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); + chatGPTrecipe.setAccountId(account.getId()); + chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); + + // Changes UI to Detailed Recipe Screen + view.goToDetailedView(chatGPTrecipe, false); + view.getDetailedView().getRecipeDetailsUI().setEditable(false); + handleDetailedViewListeners(); + } + }); + thread.start(); + AppFrameMic mic = view.getAppFrameMic(); + mic.getRecordingIngredientsLabel() + .setText("Processing mealType and ingredients. Please wait."); + mic.getRecordingIngredientsLabel() + .setStyle("-fx-font-weight: bold; -fx-font: 20 arial;"); + mic.getRecordingIngredientsLabel().setVisible(true); } catch (Exception exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); exception.printStackTrace(); @@ -335,7 +355,14 @@ private void handleDetailedViewListeners() { }); detailedView.setHomeButtonAction(this::handleHomeButton); detailedView.setShareButtonAction(this::handleShareButton); - detailedView.setRefreshButtonAction(this::handleRefreshButton); + detailedView.setRefreshButtonAction(event -> { + try { + handleRefreshButton(event); + } catch (URISyntaxException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + }); } private void handleEditButton(ActionEvent event) { @@ -360,7 +387,7 @@ private void deleteGivenRecipe(Recipe recipe) throws IOException { System.out.println("Deleting id: " + recipe.getId()); model.performRecipeRequest("DELETE", recipeStr, null); - this.view.getAppFrameHome().updateDisplay(filter); + this.view.getAppFrameHome().updateDisplay(filter); goToRecipeList(); } @@ -424,17 +451,44 @@ private void handleGoToLogin(ActionEvent event) { view.goToLoginUI(); } - private void handleRefreshButton(ActionEvent event) { + private void handleRefreshButton(ActionEvent event) throws URISyntaxException, IOException { // Get ChatGPT response from the Model if (mealType != null && ingredients != null) { + view.goToLoading(); try { - String responseText = model.performChatGPTRequest("GET", mealType, ingredients); - Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); - chatGPTrecipe.setAccountId(account.getId()); - chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); - - // Changes UI to Detailed Recipe Screen - view.getDetailedView().setRecipe(chatGPTrecipe); + Thread thread = new Thread( + new Runnable() { + @Override + public void run() { + + String responseText = model.performChatGPTRequest("GET", mealType, + ingredients); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); + chatGPTrecipe.setAccountId(account.getId()); + chatGPTrecipe.setImage(model.performDallERequest("GET", + chatGPTrecipe.getTitle())); + + // Changes UI to Detailed Recipe Screen + view.goToDetailedView(chatGPTrecipe, false); + view.getDetailedView().getRecipeDetailsUI().setEditable(false); + handleDetailedViewListeners(); + } + }); + thread.start(); + + // AppAlert.show("Loading", "Please wait for the recipe to regenerate."); + // Thread.sleep(2000); + // String responseText = model.performChatGPTRequest("GET", mealType, + // ingredients); + // Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); + // chatGPTrecipe.setAccountId(account.getId()); + // chatGPTrecipe.setImage(model.performDallERequest("GET", + // chatGPTrecipe.getTitle())); + + // // Changes UI to Detailed Recipe Screen + // view.goToDetailedView(chatGPTrecipe, false); + // view.getDetailedView().getRecipeDetailsUI().setEditable(false); + // handleDetailedViewListeners(); } catch (Exception exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); diff --git a/app/src/main/java/code/client/View/LoadingUI.java b/app/src/main/java/code/client/View/LoadingUI.java new file mode 100644 index 0000000..fcc7a62 --- /dev/null +++ b/app/src/main/java/code/client/View/LoadingUI.java @@ -0,0 +1,35 @@ +package code.client.View; + +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.geometry.Insets; +import java.io.File; + +import code.client.Model.AppConfig; + +public class LoadingUI extends HBox { + private final Label loadingLabel; + private final ImageView loadingImg; + + LoadingUI() { + GridPane gridPane = new GridPane(); + gridPane.setPrefSize(620, 620); + gridPane.setAlignment(javafx.geometry.Pos.CENTER); + gridPane.setHgap(10); + gridPane.setVgap(10); + gridPane.setPadding(new Insets(25, 25, 25, 25)); + gridPane.setStyle("-fx-background-color: #F0F8FF;"); + loadingLabel = new Label("Loading..."); + loadingLabel.setFont(Font.font("Comic Sans MS", 20)); // Adjust the size as needed + gridPane.add(loadingLabel, 1, 1); + // Show an image when the server is offline + File file = new File(AppConfig.LOADING_IMG_FILE); + loadingImg = new ImageView(new Image(file.toURI().toString())); + loadingImg.setFitWidth(Region.USE_COMPUTED_SIZE); + gridPane.add(loadingImg, 1, 2); + getChildren().addAll(gridPane); + } +} diff --git a/app/src/main/java/code/client/View/View.java b/app/src/main/java/code/client/View/View.java index c4f5cb3..48abeb8 100644 --- a/app/src/main/java/code/client/View/View.java +++ b/app/src/main/java/code/client/View/View.java @@ -14,6 +14,7 @@ public class View { private AccountCreationUI createAcc; private Scene mainScene; private OfflineUI offlineScreen; + private LoadingUI loadingUI; public View() throws IOException, URISyntaxException { offlineScreen = new OfflineUI(); @@ -22,6 +23,7 @@ public View() throws IOException, URISyntaxException { audioCapture = new AppFrameMic(); detailedRecipe = new DetailsAppFrame(); createAcc = new AccountCreationUI(); + loadingUI = new LoadingUI(); } public void setScene(Scene scene) { @@ -32,6 +34,10 @@ public void goToRecipeList() { mainScene.setRoot(home.getRoot()); } + public void goToLoading() { + mainScene.setRoot(loadingUI); + } + public void goToAudioCapture() throws URISyntaxException, IOException { audioCapture = new AppFrameMic(); mainScene.setRoot(audioCapture.getRoot()); diff --git a/app/src/main/java/code/client/View/loading.png b/app/src/main/java/code/client/View/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..5360ef77c3a3a85fab2f55e68c9a64263f9a34ea GIT binary patch literal 134091 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§oT8&4vU#~CuR48!^=Du?LwzYKZc+nZ;THAMq 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 literal 0 HcmV?d00001 From d7374e3242944694a6c2abc40f0112bde76257d4 Mon Sep 17 00:00:00 2001 From: AllKeng Date: Mon, 4 Dec 2023 23:30:11 -0800 Subject: [PATCH 14/31] Flag to turn and off mocking services. Co-authored-by: dashluu Co-authored-by: Samantha Prestrelski --- app/src/main/java/code/App.java | 10 ++++++++-- app/src/main/java/code/client/Model/AppConfig.java | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/code/App.java b/app/src/main/java/code/App.java index 0af6221..3e9a369 100644 --- a/app/src/main/java/code/App.java +++ b/app/src/main/java/code/App.java @@ -6,7 +6,9 @@ import javafx.stage.WindowEvent; import code.client.Model.*; import code.client.View.*; +import code.server.BaseServer; import code.server.AppServer; +import code.server.MockServer; import code.client.Controllers.*; import javafx.scene.Scene; import code.server.IRecipeDb; @@ -17,7 +19,7 @@ public class App extends Application { private IRecipeDb recipeDb; - private AppServer server; + private BaseServer server; @Override public void start(Stage primaryStage) throws Exception { @@ -75,6 +77,10 @@ private IRecipeDb initDb() throws IOException { } private void initServer() throws IOException { - server = new AppServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + if (AppConfig.MOCKING_ON) { + server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + } else { + server = new AppServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + } } } \ No newline at end of file diff --git a/app/src/main/java/code/client/Model/AppConfig.java b/app/src/main/java/code/client/Model/AppConfig.java index 02519d2..50d1559 100644 --- a/app/src/main/java/code/client/Model/AppConfig.java +++ b/app/src/main/java/code/client/Model/AppConfig.java @@ -7,12 +7,14 @@ public class AppConfig { public static final String RECIPE_CSV_FILE = "recipes.csv"; public static final String CREDENTIALS_CSV_FILE = "userCredentials.csv"; // API + public static final boolean MOCKING_ON = false; public static final String AUDIO_FILE = "recording.wav"; public static final AudioFileFormat.Type AUDIO_TYPE = AudioFileFormat.Type.WAVE; public static final String API_KEY = "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"; // images public static final String MICROPHONE_IMG_FILE = "app/src/main/java/code/client/View/microphone.png"; public static final String OFFLINE_IMG_FILE = "app/src/main/java/code/client/View/cat.png"; + public static final String LOADING_IMG_FILE = "app/src/main/java/code/client/View/loading.png"; public static final String RECIPE_IMG_FILE = "app/src/main/java/code/client/View/defaultRecipe.png"; // mongo public static final String MONGODB_CONN = "mongodb://trungluu:xGoGkkbozvWyiXyZ@ac-ajwebab-shard-00-00.lta1oi1.mongodb.net:27017,ac-ajwebab-shard-00-01.lta1oi1.mongodb.net:27017,ac-ajwebab-shard-00-02.lta1oi1.mongodb.net:27017/?ssl=true&replicaSet=atlas-3daxhg-shard-0&authSource=admin&retryWrites=true&w=majority"; From 0bbfa31faf9743fdc05e86f34ddbc15a7e1c158e Mon Sep 17 00:00:00 2001 From: AllKeng Date: Mon, 4 Dec 2023 23:34:31 -0800 Subject: [PATCH 15/31] Light refactoring to look pretty. Fix tests Co-authored-by: dashluu Co-authored-by: Samantha Prestrelski --- .../java/code/client/Controllers/Format.java | 9 +- .../main/java/code/client/Model/Model.java | 24 +++- .../java/code/client/View/AppFrameMic.java | 7 +- .../java/code/client/View/Ingredients.java | 2 +- .../main/java/code/client/View/MealType.java | 2 +- .../code/server/ChatGPTRequestHandler.java | 4 +- .../main/java/code/server/ShareRecipe.java | 103 +++++++++++------- .../main/java/code/server/TextToRecipe.java | 11 -- .../test/java/code/EndToEndScenario2_1.java | 9 +- app/src/test/java/code/TextToRecipeTest.java | 15 ++- 10 files changed, 112 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/code/client/Controllers/Format.java b/app/src/main/java/code/client/Controllers/Format.java index 45974e0..c070928 100644 --- a/app/src/main/java/code/client/Controllers/Format.java +++ b/app/src/main/java/code/client/Controllers/Format.java @@ -13,7 +13,7 @@ public String buildPrompt(String mealType, String ingredients) { .append(". ") .append(ingredients) .append(" Make a recipe using only these ingredients plus condiments. ") - .append("Remember to first include a title, then a list of ingredients, and then a list of instructions."); + .append("Please give me a recipe in the following format with no comments after the instructions. Title: Ingredients: Instructions:"); return prompt.toString(); } @@ -33,8 +33,11 @@ public Recipe mapResponseToRecipe(String mealType, String responseText) { } // Create a new recipe with a title - Recipe recipe = new Recipe(tokenList.get(0), mealType); - + String title = tokenList.get(0); + if (title.contains("Title:")) { + title = title.replaceAll("Title:", ""); + } + Recipe recipe = new Recipe(title.trim(), mealType); // Parse recipe's ingredients String ingredient; boolean parse = false; diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index 4906c77..cfb5b31 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -14,6 +14,8 @@ import java.net.URI; import java.nio.file.*; import java.net.URLEncoder; +import com.mongodb.MongoException; +import com.mongodb.MongoWriteException; public class Model { public String performAccountRequest(String method, String user, String password) { @@ -41,6 +43,12 @@ public String performAccountRequest(String method, String user, String password) String response = in.readLine(); in.close(); return response; + } catch (MongoWriteException ex) { + ex.printStackTrace(); + return "Duplicate Key Error"; + } catch (MongoException ex) { + ex.printStackTrace(); + return "Server Offline"; } catch (Exception ex) { ex.printStackTrace(); return "Error: " + ex.getMessage(); @@ -75,6 +83,12 @@ public String performRecipeRequest(String method, String recipe, String userId) } in.close(); return response; + } catch (MongoWriteException ex) { + ex.printStackTrace(); + return "Duplicate Key Error"; + } catch (MongoException ex) { + ex.printStackTrace(); + return "Server Offline"; } catch (Exception ex) { ex.printStackTrace(); return "Error: " + ex.getMessage(); @@ -157,13 +171,13 @@ public String performWhisperRequest(String method, String type) throws Malformed BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; response = ""; - + while ((line = in.readLine()) != null) { response += line + "\n"; } - + in.close(); - + if (type.equals("mealType") && responseCode == 200) { response = response.toUpperCase(); if (response.contains("BREAKFAST")) { @@ -173,11 +187,11 @@ public String performWhisperRequest(String method, String type) throws Malformed } else if (response.contains("DINNER")) { response = "Dinner"; } else { - response = "NONE OF THE ABOVE"; + response = null; } } } - + System.out.println("Whisper response: " + response); return response; } diff --git a/app/src/main/java/code/client/View/AppFrameMic.java b/app/src/main/java/code/client/View/AppFrameMic.java index 6916ded..e79111f 100644 --- a/app/src/main/java/code/client/View/AppFrameMic.java +++ b/app/src/main/java/code/client/View/AppFrameMic.java @@ -60,8 +60,8 @@ public class AppFrameMic extends BorderPane { recipeCreationGrid = new GridPane(); recipeCreationGrid.setAlignment(Pos.CENTER); - recipeCreationGrid.setVgap(10); - recipeCreationGrid.setHgap(10); + recipeCreationGrid.setVgap(5); + recipeCreationGrid.setHgap(5); recipeCreationGrid.setStyle("-fx-background-color: #DAE5EA; -fx-border-width: 0;"); recipeCreationGrid.add(mealTypeSelection, 0, 1); @@ -75,9 +75,6 @@ public class AppFrameMic extends BorderPane { recordIngredientsButton = ingredientsList.getRecordButton(); recordingIngredientsLabel = ingredientsList.getRecordingLabel(); - // createButton = new Button("Create Recipe"); - // recipeCreationGrid.add(createButton, 0, 3); - goToDetailedButton = new Button("See Detailed Recipe"); recipeCreationGrid.add(goToDetailedButton, 0, 5); diff --git a/app/src/main/java/code/client/View/Ingredients.java b/app/src/main/java/code/client/View/Ingredients.java index 170f7e3..bee5375 100644 --- a/app/src/main/java/code/client/View/Ingredients.java +++ b/app/src/main/java/code/client/View/Ingredients.java @@ -59,7 +59,7 @@ public class Ingredients extends GridPane { ingredientsArea.setStyle("-fx-font-size: 16"); // change ingredientsArea.setPrefWidth(300); // CHANGE 3 (WIDTH OF PROMPT) ingredientsArea.setPrefHeight(50); // CHANGE - ingredientsArea.setEditable(true); + ingredientsArea.setEditable(false); // Add all of the elements to the MealTypeSelection this.add(recordButton, 0, 0); diff --git a/app/src/main/java/code/client/View/MealType.java b/app/src/main/java/code/client/View/MealType.java index 778f657..25c306e 100644 --- a/app/src/main/java/code/client/View/MealType.java +++ b/app/src/main/java/code/client/View/MealType.java @@ -58,7 +58,7 @@ public class MealType extends GridPane { mealTypeArea.setStyle("-fx-font-size: 16"); // CHANGE 1 (FONT) mealTypeArea.setPrefWidth(300); mealTypeArea.setPrefHeight(50); - mealTypeArea.setEditable(true); + mealTypeArea.setEditable(false); // Add all of the elements to the MealTypeSelection this.add(recordButton, 0, 0); diff --git a/app/src/main/java/code/server/ChatGPTRequestHandler.java b/app/src/main/java/code/server/ChatGPTRequestHandler.java index 3c37758..48a6999 100644 --- a/app/src/main/java/code/server/ChatGPTRequestHandler.java +++ b/app/src/main/java/code/server/ChatGPTRequestHandler.java @@ -1,6 +1,7 @@ package code.server; import code.client.Model.AppConfig; +import code.client.Controllers.Format; import com.sun.net.httpserver.*; import java.io.IOException; import java.io.OutputStream; @@ -48,7 +49,8 @@ public void handle(HttpExchange httpExchange) throws IOException { private String getResponse(String mealType, String ingredients) throws IOException, InterruptedException, URISyntaxException { // Set request parameters - String prompt = buildPrompt(mealType, ingredients); + Format format = new Format(); + String prompt = format.buildPrompt(mealType, ingredients); // Create a request body which you will pass into request object JSONObject requestBody = new JSONObject(); diff --git a/app/src/main/java/code/server/ShareRecipe.java b/app/src/main/java/code/server/ShareRecipe.java index 2a7e10c..b0f9ed4 100644 --- a/app/src/main/java/code/server/ShareRecipe.java +++ b/app/src/main/java/code/server/ShareRecipe.java @@ -56,46 +56,73 @@ private static String getMockedRecipe() { private static String formatRecipe(Recipe recipe) { Iterator ingr = recipe.getIngredientIterator(); Iterator instr = recipe.getInstructionIterator(); - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder - .append("") - .append("" + recipe.getTitle() + "") - .append("") - .append("

" + recipe.getTitle() + "

") - .append("") - .append("

") - .append("Ingredients: ") - .append("

") - .append("

") - .append("

    "); - while (ingr.hasNext()) { - htmlBuilder.append("
  • " + ingr.next() + "
  • "); - } - htmlBuilder - .append("
") - .append("

") - .append("") - .append("

") - .append("Instructions: ") - .append("

") - .append("

") - .append("

    "); - while (instr.hasNext()) { - htmlBuilder.append("
  • " + instr.next() + "
  • "); - } - htmlBuilder - .append("
") - .append("

") - .append(""); - htmlBuilder - .append("") - .append(""); + return String.format(""" + + + %s + + + +

%s

+ Recipe Image +

Ingredients:

+

    + %s +

+

Instructions:

+

    + %s +

+ + + """, + recipe.getTitle(), + recipe.getTitle(), + recipe.getImage(), + buildList(ingr), + buildList(instr)); + } - // encode HTML content - return htmlBuilder.toString(); + private static String buildList(Iterator iterator) { + StringBuilder listBuilder = new StringBuilder(); + while (iterator.hasNext()) { + listBuilder.append(String.format("
  • %s
  • ", iterator.next())); + } + return listBuilder.toString(); } } diff --git a/app/src/main/java/code/server/TextToRecipe.java b/app/src/main/java/code/server/TextToRecipe.java index 53b2793..cdfbae2 100644 --- a/app/src/main/java/code/server/TextToRecipe.java +++ b/app/src/main/java/code/server/TextToRecipe.java @@ -6,17 +6,6 @@ public abstract class TextToRecipe { public abstract void handle(HttpExchange httpExchange) throws IOException; - public String buildPrompt(String mealType, String ingredients) { - StringBuilder prompt = new StringBuilder(); - prompt.append("I am a student on a budget with a busy schedule and I need to quickly cook a ") - .append(mealType) - .append(". ") - .append(ingredients) - .append(" Make a recipe using only these ingredients plus condiments. ") - .append("Remember to first include a title, then a list of ingredients, and then a list of instructions."); - return prompt.toString(); - } - public abstract void setSampleRecipe(String recipe); } diff --git a/app/src/test/java/code/EndToEndScenario2_1.java b/app/src/test/java/code/EndToEndScenario2_1.java index 2f9ad2c..9e62ce5 100644 --- a/app/src/test/java/code/EndToEndScenario2_1.java +++ b/app/src/test/java/code/EndToEndScenario2_1.java @@ -9,6 +9,7 @@ import org.bson.Document; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import static org.junit.jupiter.api.Assertions.*; import java.io.FileWriter; @@ -33,10 +34,10 @@ * 5. Saves the refreshed recipe */ public class EndToEndScenario2_1 { - private Account account; // Account used in the following tests + private static Account account; // Account used in the following tests - @BeforeEach - public void setUp() { + @BeforeAll + public static void setUp() { account = new Account("Chef", "Caitlyn"); } @@ -84,6 +85,8 @@ public void automaticLoginTest() throws IOException { @Test public void createRecipeTest() { + // RecipeBuilder builder = new RecipeBuilder(); + } /** diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index a112fcc..e3d3b61 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -27,11 +27,15 @@ public void testProvideRecipeIntegration() throws IOException, URISyntaxException, InterruptedException { // record and process audio server.start(); - String mealType = "Breakfast"; // model.performWhisperRequest("GET", "mealtype"); - String ingredients = "Chicken, eggs."; // model.performWhisperRequest("GET2", "ingredients"); + VoiceToText voiceToText = new MockWhisperRequestHandler(); + String mealType = voiceToText.processAudio("mealType"); + assertEquals("Breakfast", mealType); + + String ingredients = voiceToText.processAudio("ingredients"); + assertEquals("Chicken, eggs.", ingredients); // build prompt for chatGPT - String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Breakfast. Chicken, eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; + String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Breakfast. Chicken, eggs. Make a recipe using only these ingredients plus condiments. Please give me a recipe in the following format with no comments after the instructions. Title: Ingredients: Instructions:"; String response = format.buildPrompt(mealType, ingredients); assertEquals(prompt, response); @@ -53,9 +57,8 @@ public void testProvideRecipeIntegration() throws IOException, */ @Test public void testPromptBuild() { - TextToRecipe textToRecipe = new MockChatGPTRequestHandler(); - String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Lunch. I have rice, shrimp, chicken, and eggs. Make a recipe using only these ingredients plus condiments. Remember to first include a title, then a list of ingredients, and then a list of instructions."; - String response = textToRecipe.buildPrompt("Lunch", "I have rice, shrimp, chicken, and eggs."); + String prompt = "I am a student on a budget with a busy schedule and I need to quickly cook a Lunch. I have rice, shrimp, chicken, and eggs. Make a recipe using only these ingredients plus condiments. Please give me a recipe in the following format with no comments after the instructions. Title: Ingredients: Instructions:"; + String response = format.buildPrompt("Lunch", "I have rice, shrimp, chicken, and eggs."); assertEquals(prompt, response); } From 3e6864a24b77214e7dec62b9454f8fc0b2437771 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 00:27:11 -0800 Subject: [PATCH 16/31] fix: shareRecipe was too ambitious :( --- .../main/java/code/server/ShareRecipe.java | 53 +++++-------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/code/server/ShareRecipe.java b/app/src/main/java/code/server/ShareRecipe.java index b0f9ed4..47ce422 100644 --- a/app/src/main/java/code/server/ShareRecipe.java +++ b/app/src/main/java/code/server/ShareRecipe.java @@ -54,47 +54,20 @@ private static String getMockedRecipe() { } private static String formatRecipe(Recipe recipe) { - Iterator ingr = recipe.getIngredientIterator(); - Iterator instr = recipe.getInstructionIterator(); + String title = recipe.getTitle() != null ? recipe.getTitle() : "Untitled Recipe"; + String image = recipe.getImage() != null ? recipe.getImage() : ""; // Provide a default image or an empty string + String ingr = buildList(recipe.getIngredientIterator()); + String instr = buildList(recipe.getInstructionIterator()); return String.format(""" %s + body { font-family: 'Comic Sans MS', cursive; } + h1, h2 { font-family: 'Comic Sans MS', cursive; color: #333; } + ul { list-style-type: square; } +

    %s

    @@ -110,11 +83,11 @@ private static String formatRecipe(Recipe recipe) { """, - recipe.getTitle(), - recipe.getTitle(), - recipe.getImage(), - buildList(ingr), - buildList(instr)); + title, + title, + image, + ingr, + instr); } private static String buildList(Iterator iterator) { From 75aede722c6cf902d77c29428a44c138c847db0c Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 01:03:20 -0800 Subject: [PATCH 17/31] ci: mock whisper types --- .gitignore | 1 + app/recording.wav | Bin 0 -> 22092 bytes .../code/client/Controllers/Controller.java | 4 ++- .../main/java/code/client/Model/Model.java | 25 +++++------------- .../code/server/AccountRequestHandler.java | 1 + .../java/code/server/DallERequestHandler.java | 2 +- app/src/main/java/code/server/MockServer.java | 8 ++++++ .../server/MockWhisperRequestHandler.java | 24 ++++++----------- .../code/server/WhisperRequestHandler.java | 20 ++++++++++++-- app/src/test/java/code/TextToRecipeTest.java | 5 ++-- app/src/test/java/code/VoiceToTextTest.java | 9 +++---- 11 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 app/recording.wav diff --git a/.gitignore b/.gitignore index 0041cc0..7a0a44d 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/java,gradle *.wav +!app/recording.wav *.properties .vscode/launch.json !gradle/wrapper/gradle-wrapper.jar diff --git a/app/recording.wav b/app/recording.wav new file mode 100644 index 0000000000000000000000000000000000000000..89bf5c6e8efc4b1864b3221c61327c9e190438fa GIT binary patch literal 22092 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
    j7;9o%~xHO+PUV^h?3ng0N<3^`q&ZbtPb<`?kv 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 V16IPS21P7Id8u4ybWe%}{{u=H6l?$h literal 0 HcmV?d00001 diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index f46e8c8..e00aea5 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -654,6 +654,8 @@ private boolean performLogin(String username, String password) { if (response.contains("Offline")) { view.goToOfflineUI(); return false; + } else if (response.contains("Error")) { + return false; } else if (response.equals(AccountRequestHandler.USERNAME_NOT_FOUND) || response.equals(AccountRequestHandler.INCORRECT_PASSWORD) || response.equals(AccountRequestHandler.TAKEN_USERNAME)) { @@ -692,7 +694,7 @@ private void recordMealType() { try { mealType = model.performWhisperRequest("GET", "mealType"); - if (mealType == null) { + if (mealType.contains("Error")) { AppAlert.show("Input Error", "Please say a valid meal type!"); } mic.getMealBox().getMealType().setText(mealType); diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index cfb5b31..832edc3 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -144,16 +144,18 @@ public String performDallERequest(String method, String recipeTitle) { } public String performWhisperRequest(String method, String type) throws MalformedURLException, IOException { - String response = "Unable to perform Whisper request"; - final String postUrl = AppConfig.SERVER_URL + AppConfig.WHISPER_PATH; - final File audioFile = new File(AppConfig.AUDIO_FILE); + String response = "Error"; + String urlString = AppConfig.SERVER_URL + AppConfig.WHISPER_PATH; + urlString += "?=" + type; String boundary = Long.toHexString(System.currentTimeMillis()); String CRLF = "\r\n"; String charset = "UTF-8"; - URLConnection connection = new URL(postUrl).openConnection(); + URLConnection connection = new URL(urlString).openConnection(); connection.setDoOutput(true); connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + // send Whisper Request + File audioFile = new File(AppConfig.AUDIO_FILE); try (OutputStream output = connection.getOutputStream(); PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);) { writer.append("--" + boundary).append(CRLF); @@ -177,22 +179,9 @@ public String performWhisperRequest(String method, String type) throws Malformed } in.close(); - - if (type.equals("mealType") && responseCode == 200) { - response = response.toUpperCase(); - if (response.contains("BREAKFAST")) { - response = "Breakfast"; - } else if (response.contains("LUNCH")) { - response = "Lunch"; - } else if (response.contains("DINNER")) { - response = "Dinner"; - } else { - response = null; - } - } } System.out.println("Whisper response: " + response); - return response; + return response.trim(); } } \ No newline at end of file diff --git a/app/src/main/java/code/server/AccountRequestHandler.java b/app/src/main/java/code/server/AccountRequestHandler.java index e8d2b84..20816cf 100644 --- a/app/src/main/java/code/server/AccountRequestHandler.java +++ b/app/src/main/java/code/server/AccountRequestHandler.java @@ -33,6 +33,7 @@ public void handle(HttpExchange httpExchange) throws IOException { } catch (MongoTimeoutException e) { response = "Server Offline"; } catch (Exception e) { + response = "Error"; System.out.println("An erroneous request"); e.printStackTrace(); } diff --git a/app/src/main/java/code/server/DallERequestHandler.java b/app/src/main/java/code/server/DallERequestHandler.java index fdbb324..6e17245 100644 --- a/app/src/main/java/code/server/DallERequestHandler.java +++ b/app/src/main/java/code/server/DallERequestHandler.java @@ -30,7 +30,7 @@ public void handle(HttpExchange httpExchange) throws IOException { recipeTitle = URLEncoder.encode(recipeTitle, "UTF-8"); response = getResponse(recipeTitle); } catch (InterruptedException e) { - response = "An error occurred."; + response = "Error"; e.printStackTrace(); } diff --git a/app/src/main/java/code/server/MockServer.java b/app/src/main/java/code/server/MockServer.java index be019da..eeaeb08 100644 --- a/app/src/main/java/code/server/MockServer.java +++ b/app/src/main/java/code/server/MockServer.java @@ -13,6 +13,8 @@ import java.net.URISyntaxException; import java.util.concurrent.*; +import org.bson.Document; + public class MockServer extends BaseServer { private IRecipeDb recipeDb; private AccountMongoDB accountMongoDB; @@ -22,6 +24,12 @@ public class MockServer extends BaseServer { public MockServer(String hostName, int port) { super(hostName, port); + MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN); + MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); + MongoCollection userCollection = mongoDb.getCollection(AppConfig.MONGO_USER_COLLECTION); + MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); + accountMongoDB = new AccountMongoDB(userCollection); + recipeDb = new RecipeMongoDb(recipeCollection); } @Override diff --git a/app/src/main/java/code/server/MockWhisperRequestHandler.java b/app/src/main/java/code/server/MockWhisperRequestHandler.java index f9771a0..a225dc8 100644 --- a/app/src/main/java/code/server/MockWhisperRequestHandler.java +++ b/app/src/main/java/code/server/MockWhisperRequestHandler.java @@ -3,7 +3,7 @@ import com.sun.net.httpserver.*; import java.io.IOException; import java.io.OutputStream; -import java.net.URISyntaxException; +import java.net.URI; public class MockWhisperRequestHandler extends VoiceToText implements HttpHandler { public MockWhisperRequestHandler() { @@ -14,24 +14,16 @@ public MockWhisperRequestHandler(IHttpConnection connection) { super(connection); } - public String processAudio(String type) throws IOException, URISyntaxException { - if (type.equals("mealType")) { - // processed correctly - return "Breakfast"; - } else if (type.equals("ingredients")) { - return "Chicken, eggs."; - } else { - return "Error text"; - } - } - @Override public void handle(HttpExchange httpExchange) throws IOException { - String method = httpExchange.getRequestMethod(); - String response = "Request received"; - if (method.equals("GET")) { + URI uri = httpExchange.getRequestURI(); + String query = uri.getRawQuery(); + String type = query.substring(query.indexOf("=") + 1); + + String response = ""; + if (type.equals("mealType")) { response = "Breakfast"; - } else if (method.equals("GET2")) { + } else if (type.equals("ingredients")) { response = "Chicken, eggs."; } diff --git a/app/src/main/java/code/server/WhisperRequestHandler.java b/app/src/main/java/code/server/WhisperRequestHandler.java index ab219b7..50f3840 100644 --- a/app/src/main/java/code/server/WhisperRequestHandler.java +++ b/app/src/main/java/code/server/WhisperRequestHandler.java @@ -5,14 +5,17 @@ import code.client.Model.AppConfig; import java.io.*; +import java.net.URI; public class WhisperRequestHandler implements HttpHandler { @Override public void handle(HttpExchange httpExchange) throws IOException { String response = "Request received"; - String method = httpExchange.getRequestMethod(); - System.out.println("Method is " + method); + URI uri = httpExchange.getRequestURI(); + String query = uri.getRawQuery(); + String type = query.substring(query.indexOf("=") + 1); + int audioFileSize = 0; try { @@ -49,10 +52,23 @@ public void handle(HttpExchange httpExchange) throws IOException { VoiceToText whisperService = new WhisperService(); response = whisperService.processAudio(""); } catch (Exception e) { + response = "Error"; System.out.println("An erroneous request"); e.printStackTrace(); } + if (type.equals("mealType")) { + response = response.toUpperCase(); + if (response.contains("BREAKFAST")) { + response = "Breakfast"; + } else if (response.contains("LUNCH")) { + response = "Lunch"; + } else if (response.contains("DINNER")) { + response = "Dinner"; + } else { + response = "Error"; + } + } // Sending back response to the client httpExchange.sendResponseHeaders(200, response.length()); OutputStream outStream = httpExchange.getResponseBody(); diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index e3d3b61..9d3567a 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -27,11 +27,10 @@ public void testProvideRecipeIntegration() throws IOException, URISyntaxException, InterruptedException { // record and process audio server.start(); - VoiceToText voiceToText = new MockWhisperRequestHandler(); - String mealType = voiceToText.processAudio("mealType"); + String mealType = model.performWhisperRequest("GET", "mealType"); assertEquals("Breakfast", mealType); - String ingredients = voiceToText.processAudio("ingredients"); + String ingredients = model.performWhisperRequest("GET", "ingredients"); assertEquals("Chicken, eggs.", ingredients); // build prompt for chatGPT diff --git a/app/src/test/java/code/VoiceToTextTest.java b/app/src/test/java/code/VoiceToTextTest.java index 538d802..420518d 100644 --- a/app/src/test/java/code/VoiceToTextTest.java +++ b/app/src/test/java/code/VoiceToTextTest.java @@ -11,8 +11,6 @@ import code.server.IHttpConnection; import code.server.MockHttpConnection; import code.server.MockServer; -import code.server.MockWhisperRequestHandler; -import code.server.VoiceToText; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -25,12 +23,13 @@ public class VoiceToTextTest { */ @Test void testSuccessfulProcessAudio() throws IOException, URISyntaxException { - VoiceToText voiceToText = new MockWhisperRequestHandler(); - String response = voiceToText.processAudio("mealType"); + server.start(); + String response = model.performWhisperRequest("GET", "mealType"); assertEquals("Breakfast", response); - response = voiceToText.processAudio("ingredients"); + response = model.performWhisperRequest("GET", "ingredients"); assertEquals("Chicken, eggs.", response); + server.stop(); } /* From f402f31d14e7f81cb60ff554032ec2a0ab9fb936 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 02:36:57 -0800 Subject: [PATCH 18/31] fix: threaded acc creation and login, error handling offlineUI --- .../code/client/Controllers/Controller.java | 118 +++++++++++------- .../main/java/code/client/Model/Model.java | 26 ++-- .../code/client/View/DetailsAppFrame.java | 6 +- .../java/code/client/View/MealTagStyler.java | 6 +- app/src/main/java/code/client/View/View.java | 9 ++ .../code/server/AccountRequestHandler.java | 7 +- .../code/server/RecipeRequestHandler.java | 9 ++ 7 files changed, 110 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index e00aea5..5bcee69 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -13,6 +13,7 @@ import java.net.URL; import javafx.animation.*; +import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.geometry.Pos; @@ -116,14 +117,12 @@ private void handleRecipePostButton(ActionEvent event) throws IOException { recipeWriter.writeRecipe(postedRecipe); String recipe = writer.toString(); - // Debugging - // System.out.println("Posting: " + recipe); String response = model.performRecipeRequest("POST", recipe, null); if (response.contains("Offline")) { - + AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); } else if (response.contains("Error")) { - + AppAlert.show("Error", "Something went wrong. Please check your inputs and try again."); } } @@ -359,7 +358,6 @@ private void handleDetailedViewListeners() { try { handleRefreshButton(event); } catch (URISyntaxException | IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } }); @@ -460,7 +458,6 @@ private void handleRefreshButton(ActionEvent event) throws URISyntaxException, I new Runnable() { @Override public void run() { - String responseText = model.performChatGPTRequest("GET", mealType, ingredients); Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); @@ -476,20 +473,6 @@ public void run() { }); thread.start(); - // AppAlert.show("Loading", "Please wait for the recipe to regenerate."); - // Thread.sleep(2000); - // String responseText = model.performChatGPTRequest("GET", mealType, - // ingredients); - // Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); - // chatGPTrecipe.setAccountId(account.getId()); - // chatGPTrecipe.setImage(model.performDallERequest("GET", - // chatGPTrecipe.getTitle())); - - // // Changes UI to Detailed Recipe Screen - // view.goToDetailedView(chatGPTrecipe, false); - // view.getDetailedView().getRecipeDetailsUI().setEditable(false); - // handleDetailedViewListeners(); - } catch (Exception exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); exception.printStackTrace(); @@ -509,17 +492,42 @@ private void handleCreateAcc(ActionEvent event) { if (username.isEmpty() || password.isEmpty()) { // Display an error message if username or password is empty showErrorPane(grid, "Error. Please provide a username and password."); - } else if (isUsernameTaken(username, password)) { - // Display an error message if the username is already taken - showErrorPane(grid, "Error. This username is already taken. Please choose another one."); } else { - // Continue with account creation logic - System.out.println("Account Created!\nUsername: " + username + "\nPassword: " + password); - model.performAccountRequest("PUT", username, password); - // Show success message - showSuccessPane(grid); - view.goToLoginUI(); + view.goToLoading(); + Thread thread = new Thread( + new Runnable() { + @Override + public void run() { + if (!isUsernameTaken(username, password) + && !view.getMainScene().equals(view.getOfflineUI())) { + // Continue with account creation logic + System.out.println( + "Username not taken!\nUsername: " + username + "\nPassword: " + password); + String response = model.performAccountRequest("PUT", username, password); + // Show success message + if (response.contains("Offline")) { + view.goToOfflineUI(); + } else { + showSuccessPane(grid); + view.goToLoginUI(); + } + + } else { + // Display an error message if the username is already taken + view.goToCreateAcc(); + Platform.runLater(new Runnable() { + @Override + public void run() { + showErrorPane(grid, + "Error. This username is already taken. Please choose another one."); + } + }); + } + } + }); + thread.start(); } + } private void showErrorPane(GridPane grid, String errorMessage) { @@ -552,13 +560,12 @@ private void showSuccessPane(GridPane grid) { private boolean isUsernameTaken(String username, String password) { // Check if the username is already taken - // temporary logic, no database yet String response = model.performAccountRequest("GET", username, password); if (response.contains("Offline")) { view.goToOfflineUI(); + return true; } - // System.out.println("Response for usernameTaken : " + response); - return (response.equals("Username is taken")); + return (!response.equals("Username is not found")); } //////////////////////////////////////// @@ -571,20 +578,39 @@ private void handleLoginButton(ActionEvent event) { // Display an error message if username or password is empty showErrorPane(grid, "Error. Please provide a username and password."); } else { - boolean loginSuccessful = performLogin(username, password); - - if (loginSuccessful) { - showLoginSuccessPane(grid, true); // useless - - goToRecipeList(); - if (!view.getLoginUI().getRememberLogin()) { - clearCredentials(); - } else { - saveCredentials(account); - } - } else { - showLoginSuccessPane(grid, false); - } + view.goToLoading(); + Thread thread = new Thread( + new Runnable() { + @Override + public void run() { + boolean loginSuccessful = performLogin(username, password); + if (loginSuccessful) { + Platform.runLater(new Runnable() { + @Override + public void run() { + goToRecipeList(); + if (!view.getLoginUI().getRememberLogin()) { + clearCredentials(); + } else { + saveCredentials(account); + } + } + }); + + } else { + Platform.runLater(new Runnable() { + @Override + public void run() { + if (!view.getMainScene().equals(view.getOfflineUI())) { + view.goToLoginUI(); + showLoginSuccessPane(grid, false); + } + } + }); + } + } + }); + thread.start(); } } diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index 832edc3..70710a7 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -15,10 +15,12 @@ import java.nio.file.*; import java.net.URLEncoder; import com.mongodb.MongoException; +import com.mongodb.MongoSocketReadException; import com.mongodb.MongoWriteException; public class Model { public String performAccountRequest(String method, String user, String password) { + String response = "Error"; try { String urlString = AppConfig.SERVER_URL + AppConfig.ACCOUNT_PATH; urlString += "?=" + user + ":" + password; @@ -40,23 +42,18 @@ public String performAccountRequest(String method, String user, String password) } BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String response = in.readLine(); + response = in.readLine(); in.close(); - return response; - } catch (MongoWriteException ex) { - ex.printStackTrace(); - return "Duplicate Key Error"; - } catch (MongoException ex) { - ex.printStackTrace(); - return "Server Offline"; } catch (Exception ex) { ex.printStackTrace(); - return "Error: " + ex.getMessage(); + response = "Error: " + ex.getMessage(); } + return response; } public String performRecipeRequest(String method, String recipe, String userId) { // Implement your HTTP request logic here and return the response + String response = "Error"; try { String urlString = AppConfig.SERVER_URL + AppConfig.RECIPE_PATH; if (userId != null) { @@ -76,23 +73,16 @@ public String performRecipeRequest(String method, String recipe, String userId) } BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String response = ""; String line; while ((line = in.readLine()) != null) { response += line + "\n"; } in.close(); - return response; - } catch (MongoWriteException ex) { - ex.printStackTrace(); - return "Duplicate Key Error"; - } catch (MongoException ex) { - ex.printStackTrace(); - return "Server Offline"; } catch (Exception ex) { ex.printStackTrace(); - return "Error: " + ex.getMessage(); + response = "Error: " + ex.getMessage(); } + return response; } public String performChatGPTRequest(String method, String mealType, String ingredients) { diff --git a/app/src/main/java/code/client/View/DetailsAppFrame.java b/app/src/main/java/code/client/View/DetailsAppFrame.java index ad81cc0..51bbc46 100644 --- a/app/src/main/java/code/client/View/DetailsAppFrame.java +++ b/app/src/main/java/code/client/View/DetailsAppFrame.java @@ -27,9 +27,9 @@ public DetailsAppFrame() { detailedUI.setStyle("-fx-background-color: #F0F8FF;"); setupGrowingUI(); - defaultButtonStyle = "-fx-font-style: italic; -fx-background-color: #FFFFFF; -fx-font-weight: bold; -fx-font: 11 arial;"; - onStyle = "-fx-font-style: italic; -fx-background-color: #90EE90; -fx-font-weight: bold; -fx-font: 11 arial;"; - offStyle = "-fx-font-style: italic; -fx-background-color: #FF7377; -fx-font-weight: bold; -fx-font: 11 arial;"; + defaultButtonStyle = "-fx-font: italic 11 arial; -fx-background-color: #FFFFFF; -fx-font-weight: bold;"; + onStyle = "-fx-font: italic 11 arial; -fx-background-color: #90EE90; -fx-font-weight: bold;"; + offStyle = "-fx-font: italic 11 arial; -fx-background-color: #FF7377; -fx-font-weight: bold;"; backToHomeButton = new Button("Back to List"); backToHomeButton.setStyle(defaultButtonStyle); diff --git a/app/src/main/java/code/client/View/MealTagStyler.java b/app/src/main/java/code/client/View/MealTagStyler.java index d27791e..0092e34 100644 --- a/app/src/main/java/code/client/View/MealTagStyler.java +++ b/app/src/main/java/code/client/View/MealTagStyler.java @@ -8,19 +8,19 @@ public static void styleTags(Recipe recipe, Button mealType) { switch (recipe.getMealTag().toLowerCase()) { case "breakfast": mealType.setStyle( - "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #FF7276; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); + "-fx-text-fill: #000000; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #FF7276; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); mealType.setText("Breakfast"); break; case "lunch": mealType.setStyle( - "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FFFF; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); + "-fx-text-fill: #000000; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FFFF; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); mealType.setText("Lunch"); break; case "dinner": mealType.setStyle( - "-fx-text-fill: black; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FF00; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); + "-fx-text-fill: #000000; -fx-font: 12 arial; -fx-font-weight: bold; -fx-background-color: #00FF00; -fx-border-width: 0; -fx-background-radius: 150; -fx-pref-width: 100; -fx-pref-height: 50;"); mealType.setText("Dinner"); break; } diff --git a/app/src/main/java/code/client/View/View.java b/app/src/main/java/code/client/View/View.java index 48abeb8..459db44 100644 --- a/app/src/main/java/code/client/View/View.java +++ b/app/src/main/java/code/client/View/View.java @@ -4,6 +4,7 @@ import java.net.URISyntaxException; import code.server.Recipe; +import javafx.scene.Parent; import javafx.scene.Scene; public class View { @@ -26,6 +27,10 @@ public View() throws IOException, URISyntaxException { loadingUI = new LoadingUI(); } + public Parent getMainScene() { + return mainScene.getRoot(); + } + public void setScene(Scene scene) { mainScene = scene; } @@ -59,6 +64,10 @@ public void goToOfflineUI() { mainScene.setRoot(offlineScreen); } + public OfflineUI getOfflineUI() { + return offlineScreen; + } + public RecipeListUI getRecipeButtons() { return home.getRecipeList(); } diff --git a/app/src/main/java/code/server/AccountRequestHandler.java b/app/src/main/java/code/server/AccountRequestHandler.java index 20816cf..c21d217 100644 --- a/app/src/main/java/code/server/AccountRequestHandler.java +++ b/app/src/main/java/code/server/AccountRequestHandler.java @@ -1,6 +1,8 @@ package code.server; +import com.mongodb.MongoSocketReadException; import com.mongodb.MongoTimeoutException; +import com.mongodb.MongoWriteException; import com.sun.net.httpserver.*; import java.io.*; @@ -30,7 +32,10 @@ public void handle(HttpExchange httpExchange) throws IOException { } else { throw new Exception("Not valid request method."); } - } catch (MongoTimeoutException e) { + } catch (MongoWriteException ex) { + ex.printStackTrace(); + response = "Duplicate Key Error"; + } catch (MongoSocketReadException | MongoTimeoutException e) { response = "Server Offline"; } catch (Exception e) { response = "Error"; diff --git a/app/src/main/java/code/server/RecipeRequestHandler.java b/app/src/main/java/code/server/RecipeRequestHandler.java index 0e3f3c1..dafc403 100644 --- a/app/src/main/java/code/server/RecipeRequestHandler.java +++ b/app/src/main/java/code/server/RecipeRequestHandler.java @@ -1,5 +1,7 @@ package code.server; +import com.mongodb.MongoSocketReadException; +import com.mongodb.MongoWriteException; import com.sun.net.httpserver.*; import code.client.Model.RecipeCSVWriter; @@ -31,7 +33,14 @@ public void handle(HttpExchange httpExchange) throws IOException { } else { throw new Exception("Not valid request method."); } + } catch (MongoWriteException ex) { + ex.printStackTrace(); + response = "Duplicate Key Error"; + } catch (MongoSocketReadException ex) { + ex.printStackTrace(); + response = "Server Offline"; } catch (Exception e) { + response = "Error"; System.out.println("An erroneous request"); e.printStackTrace(); } From 347a57b6a5889e47f3921ff8445c957742c833d7 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 11:17:27 -0800 Subject: [PATCH 19/31] refactor: shortened thread running and sort/filter --- .../code/client/Controllers/Controller.java | 250 +++++++----------- 1 file changed, 92 insertions(+), 158 deletions(-) diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index 5bcee69..e942559 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -183,34 +183,17 @@ private void addFilterListeners() { MenuButton filterMenuButton = this.view.getAppFrameHome().getFilterMenuButton(); ObservableList filterMenuItems = filterMenuButton.getItems(); - // Filter to show only breakfast recipes when criteria is selected - filterMenuItems.get(BREAKFAST_INDEX).setOnAction(e -> { - filter = "breakfast"; - setActiveState(filterMenuButton, BREAKFAST_INDEX); - this.view.getAppFrameHome().updateDisplay("breakfast"); - addListenersToList(); - }); - // Filter to show only lunch recipes when criteria is selected - filterMenuItems.get(LUNCH_INDEX).setOnAction(e -> { - filter = "lunch"; - setActiveState(filterMenuButton, LUNCH_INDEX); - this.view.getAppFrameHome().updateDisplay("lunch"); - addListenersToList(); - }); - // Filter to show only dinner recipes when criteria is selected - filterMenuItems.get(DINNER_INDEX).setOnAction(e -> { - filter = "dinner"; - setActiveState(filterMenuButton, DINNER_INDEX); - this.view.getAppFrameHome().updateDisplay("dinner"); - addListenersToList(); - }); - // Remove selected filter to show all recipes - filterMenuItems.get(NONE_INDEX).setOnAction(e -> { - filter = "none"; - setActiveState(filterMenuButton, NONE_INDEX); - this.view.getAppFrameHome().updateDisplay("none"); - addListenersToList(); - }); + String[] filterTypes = { "breakfast", "lunch", "dinner", "none" }; + + for (int i = 0; i < filterTypes.length; i++) { + int index = i; + filterMenuItems.get(index).setOnAction(e -> { + filter = filterTypes[index]; + setActiveState(filterMenuButton, index); + this.view.getAppFrameHome().updateDisplay(filterTypes[index]); + addListenersToList(); + }); + } } private void addSortingListener() { @@ -222,25 +205,16 @@ private void addSortingListener() { } RecipeSorter recipeSorter = new RecipeSorter(list.getRecipeDB().getList()); - // Setting action for newest to oldest sorting criteria - sortMenuItems.get(NEWEST_TO_OLDEST_INDEX).setOnAction(e -> { - recipeSorter.sortNewestToOldest(); - sortList(sortMenuButton, NEWEST_TO_OLDEST_INDEX); - }); - // Setting action for oldest to newest sorting criteria - sortMenuItems.get(OLDEST_TO_NEWEST_INDEX).setOnAction(e -> { - recipeSorter.sortOldestToNewest(); - sortList(sortMenuButton, OLDEST_TO_NEWEST_INDEX); - }); - // Setting action for A to Z sorting criteria - sortMenuItems.get(A_TO_Z_INDEX).setOnAction(e -> { - recipeSorter.sortAToZ(); - sortList(sortMenuButton, A_TO_Z_INDEX); - }); - // Setting action for Z to A sorting criteria - sortMenuItems.get(Z_TO_A_INDEX).setOnAction(e -> { - recipeSorter.sortZToA(); - sortList(sortMenuButton, Z_TO_A_INDEX); + setSortAction(sortMenuItems, NEWEST_TO_OLDEST_INDEX, recipeSorter::sortNewestToOldest); + setSortAction(sortMenuItems, OLDEST_TO_NEWEST_INDEX, recipeSorter::sortOldestToNewest); + setSortAction(sortMenuItems, A_TO_Z_INDEX, recipeSorter::sortAToZ); + setSortAction(sortMenuItems, Z_TO_A_INDEX, recipeSorter::sortZToA); + } + + private void setSortAction(ObservableList sortMenuItems, int index, Runnable sortAction) { + sortMenuItems.get(index).setOnAction(e -> { + sortAction.run(); + sortList(view.getAppFrameHome().getSortMenuButton(), index); }); } @@ -300,21 +274,17 @@ private void handleDetailedViewFromNewRecipeButton(ActionEvent event) { if (mealType != null && ingredients != null) { view.goToLoading(); try { - Thread thread = new Thread( - new Runnable() { - @Override - public void run() { - String responseText = model.performChatGPTRequest("GET", mealType, ingredients); - Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); - chatGPTrecipe.setAccountId(account.getId()); - chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); - - // Changes UI to Detailed Recipe Screen - view.goToDetailedView(chatGPTrecipe, false); - view.getDetailedView().getRecipeDetailsUI().setEditable(false); - handleDetailedViewListeners(); - } - }); + Thread thread = new Thread(() -> { + String responseText = model.performChatGPTRequest("GET", mealType, ingredients); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); + chatGPTrecipe.setAccountId(account.getId()); + chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); + + // Changes UI to Detailed Recipe Screen + view.goToDetailedView(chatGPTrecipe, false); + view.getDetailedView().getRecipeDetailsUI().setEditable(false); + handleDetailedViewListeners(); + }); thread.start(); AppFrameMic mic = view.getAppFrameMic(); mic.getRecordingIngredientsLabel() @@ -326,7 +296,9 @@ public void run() { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); exception.printStackTrace(); } - } else { + } else + + { AppAlert.show("Input Error", "Invalid meal type or ingredients, please try again!"); } } @@ -454,25 +426,20 @@ private void handleRefreshButton(ActionEvent event) throws URISyntaxException, I if (mealType != null && ingredients != null) { view.goToLoading(); try { - Thread thread = new Thread( - new Runnable() { - @Override - public void run() { - String responseText = model.performChatGPTRequest("GET", mealType, - ingredients); - Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); - chatGPTrecipe.setAccountId(account.getId()); - chatGPTrecipe.setImage(model.performDallERequest("GET", - chatGPTrecipe.getTitle())); - - // Changes UI to Detailed Recipe Screen - view.goToDetailedView(chatGPTrecipe, false); - view.getDetailedView().getRecipeDetailsUI().setEditable(false); - handleDetailedViewListeners(); - } - }); + Thread thread = new Thread(() -> { + String responseText = model.performChatGPTRequest("GET", mealType, + ingredients); + Recipe chatGPTrecipe = format.mapResponseToRecipe(mealType, responseText); + chatGPTrecipe.setAccountId(account.getId()); + chatGPTrecipe.setImage(model.performDallERequest("GET", + chatGPTrecipe.getTitle())); + + // Changes UI to Detailed Recipe Screen + view.goToDetailedView(chatGPTrecipe, false); + view.getDetailedView().getRecipeDetailsUI().setEditable(false); + handleDetailedViewListeners(); + }); thread.start(); - } catch (Exception exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); exception.printStackTrace(); @@ -484,50 +451,35 @@ public void run() { //////////////////////////////////////// private void handleCreateAcc(ActionEvent event) { - // Simulating existing usernames GridPane grid = view.getAccountCreationUI().getRoot(); String username = view.getAccountCreationUI().getUsernameTextField().getText(); String password = view.getAccountCreationUI().getPasswordField().getText(); if (username.isEmpty() || password.isEmpty()) { - // Display an error message if username or password is empty showErrorPane(grid, "Error. Please provide a username and password."); - } else { - view.goToLoading(); - Thread thread = new Thread( - new Runnable() { - @Override - public void run() { - if (!isUsernameTaken(username, password) - && !view.getMainScene().equals(view.getOfflineUI())) { - // Continue with account creation logic - System.out.println( - "Username not taken!\nUsername: " + username + "\nPassword: " + password); - String response = model.performAccountRequest("PUT", username, password); - // Show success message - if (response.contains("Offline")) { - view.goToOfflineUI(); - } else { - showSuccessPane(grid); - view.goToLoginUI(); - } - - } else { - // Display an error message if the username is already taken - view.goToCreateAcc(); - Platform.runLater(new Runnable() { - @Override - public void run() { - showErrorPane(grid, - "Error. This username is already taken. Please choose another one."); - } - }); - } - } - }); - thread.start(); + return; } + view.goToLoading(); + + Thread thread = new Thread(() -> { + boolean isUsernameTaken = isUsernameTaken(username, password); + if (view.getMainScene().equals(view.getOfflineUI())) { + } else if (!isUsernameTaken) { + String response = model.performAccountRequest("PUT", username, password); + if (response.contains("Offline")) { + view.goToOfflineUI(); + } else { + showSuccessPane(grid); + view.goToLoginUI(); + } + } else { + view.goToCreateAcc(); + Platform.runLater( + () -> showErrorPane(grid, "Error. This username is already taken. Please choose another one.")); + } + }); + thread.start(); } private void showErrorPane(GridPane grid, String errorMessage) { @@ -563,12 +515,12 @@ private boolean isUsernameTaken(String username, String password) { String response = model.performAccountRequest("GET", username, password); if (response.contains("Offline")) { view.goToOfflineUI(); - return true; + return false; } return (!response.equals("Username is not found")); } - //////////////////////////////////////// + //////////////////////////////////////// private void handleLoginButton(ActionEvent event) { String username = view.getLoginUI().getUsernameTextField().getText(); String password = view.getLoginUI().getPasswordField().getText(); @@ -577,41 +529,28 @@ private void handleLoginButton(ActionEvent event) { if (username.isEmpty() || password.isEmpty()) { // Display an error message if username or password is empty showErrorPane(grid, "Error. Please provide a username and password."); - } else { - view.goToLoading(); - Thread thread = new Thread( - new Runnable() { - @Override - public void run() { - boolean loginSuccessful = performLogin(username, password); - if (loginSuccessful) { - Platform.runLater(new Runnable() { - @Override - public void run() { - goToRecipeList(); - if (!view.getLoginUI().getRememberLogin()) { - clearCredentials(); - } else { - saveCredentials(account); - } - } - }); - - } else { - Platform.runLater(new Runnable() { - @Override - public void run() { - if (!view.getMainScene().equals(view.getOfflineUI())) { - view.goToLoginUI(); - showLoginSuccessPane(grid, false); - } - } - }); - } - } - }); - thread.start(); + return; } + + view.goToLoading(); + Thread thread = new Thread(() -> { + boolean loginSuccessful = performLogin(username, password); + if (view.getMainScene().equals(view.getOfflineUI())) { + } else if (loginSuccessful) { + Platform.runLater( + () -> goToRecipeList()); + if (!view.getLoginUI().getRememberLogin()) { + clearCredentials(); + } else { + saveCredentials(account); + } + } else { + view.goToLoginUI(); + Platform.runLater( + () -> showLoginSuccessPane(grid, false)); + } + }); + thread.start(); } private void clearCredentials() { @@ -710,13 +649,11 @@ private void recordMealType() { recording = true; mic.getRecordMealTypeButton().setStyle("-fx-background-color: #FF0000;"); mic.getRecordingMealTypeLabel().setVisible(true); - // recordingLabel1.setStyle("-fx-font-color: #FF0000;"); } else { recorder.stopRecording(); recording = false; mic.getRecordMealTypeButton().setStyle(""); mic.getRecordingMealTypeLabel().setVisible(false); - // recordingLabel1.setStyle(""); try { mealType = model.performWhisperRequest("GET", "mealType"); @@ -738,13 +675,11 @@ private void recordIngredients() { recording = true; mic.getRecordIngredientsButton().setStyle("-fx-background-color: #FF0000;"); mic.getRecordingIngredientsLabel().setVisible(true); - // recordingLabel2.setStyle("-fx-background-color: #FF0000;"); } else { recorder.stopRecording(); recording = false; mic.getRecordIngredientsButton().setStyle(""); mic.getRecordingIngredientsLabel().setVisible(false); - // recordingLabel2.setStyle(""); try { ingredients = model.performWhisperRequest("GET", "ingredients"); @@ -764,7 +699,6 @@ private void recordIngredients() { } } } - /////////////////////////////// AUDIOMANAGEMENT////////////////////////////////// } \ No newline at end of file From 9329890dca82ecd1d647f51423198d69c1d3253b Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 11:27:01 -0800 Subject: [PATCH 20/31] refactor: move images --- app/src/main/java/code/client/View/Ingredients.java | 2 +- .../main/java/code/client/View/{ => images}/cat.png | Bin .../code/client/View/{ => images}/defaultRecipe.png | Bin .../java/code/client/View/{ => images}/loading.png | Bin .../code/client/View/{ => images}/microphone.png | Bin 5 files changed, 1 insertion(+), 1 deletion(-) rename app/src/main/java/code/client/View/{ => images}/cat.png (100%) rename app/src/main/java/code/client/View/{ => images}/defaultRecipe.png (100%) rename app/src/main/java/code/client/View/{ => images}/loading.png (100%) rename app/src/main/java/code/client/View/{ => images}/microphone.png (100%) diff --git a/app/src/main/java/code/client/View/Ingredients.java b/app/src/main/java/code/client/View/Ingredients.java index bee5375..68df493 100644 --- a/app/src/main/java/code/client/View/Ingredients.java +++ b/app/src/main/java/code/client/View/Ingredients.java @@ -30,7 +30,7 @@ public class Ingredients extends GridPane { this.setHgap(20); // Get a picture of a microphone for the voice recording button - File file = new File("app/src/main/java/code/client/View/microphone.png"); + File file = new File(AppConfig.MICROPHONE_IMG_FILE); microphone = new ImageView(new Image(file.toURI().toString())); // Set the size of the microphone image diff --git a/app/src/main/java/code/client/View/cat.png b/app/src/main/java/code/client/View/images/cat.png similarity index 100% rename from app/src/main/java/code/client/View/cat.png rename to app/src/main/java/code/client/View/images/cat.png diff --git a/app/src/main/java/code/client/View/defaultRecipe.png b/app/src/main/java/code/client/View/images/defaultRecipe.png similarity index 100% rename from app/src/main/java/code/client/View/defaultRecipe.png rename to app/src/main/java/code/client/View/images/defaultRecipe.png diff --git a/app/src/main/java/code/client/View/loading.png b/app/src/main/java/code/client/View/images/loading.png similarity index 100% rename from app/src/main/java/code/client/View/loading.png rename to app/src/main/java/code/client/View/images/loading.png diff --git a/app/src/main/java/code/client/View/microphone.png b/app/src/main/java/code/client/View/images/microphone.png similarity index 100% rename from app/src/main/java/code/client/View/microphone.png rename to app/src/main/java/code/client/View/images/microphone.png From d37c849482a42a2a0c4f78b15157630e0ea1401e Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 11:39:04 -0800 Subject: [PATCH 21/31] refactor!: move mocking to separate folder --- app/src/main/java/code/App.java | 2 +- .../java/code/server/ShareLinkMongoDb.java | 41 ------------------- .../MockChatGPTRequestHandler.java | 4 +- .../MockDallERequestHandler.java | 4 +- .../{ => mocking}/MockHttpConnection.java | 4 +- .../code/server/{ => mocking}/MockServer.java | 9 +++- .../MockWhisperRequestHandler.java | 6 ++- app/src/test/java/code/RecipeToImageTest.java | 2 +- app/src/test/java/code/RefreshTest.java | 1 + .../test/java/code/ServerConnectionTest.java | 2 +- app/src/test/java/code/TextToRecipeTest.java | 1 + app/src/test/java/code/VoiceToTextTest.java | 4 +- 12 files changed, 29 insertions(+), 51 deletions(-) delete mode 100644 app/src/main/java/code/server/ShareLinkMongoDb.java rename app/src/main/java/code/server/{ => mocking}/MockChatGPTRequestHandler.java (95%) rename app/src/main/java/code/server/{ => mocking}/MockDallERequestHandler.java (95%) rename app/src/main/java/code/server/{ => mocking}/MockHttpConnection.java (94%) rename app/src/main/java/code/server/{ => mocking}/MockServer.java (90%) rename app/src/main/java/code/server/{ => mocking}/MockWhisperRequestHandler.java (91%) diff --git a/app/src/main/java/code/App.java b/app/src/main/java/code/App.java index 3e9a369..8462006 100644 --- a/app/src/main/java/code/App.java +++ b/app/src/main/java/code/App.java @@ -8,10 +8,10 @@ import code.client.View.*; import code.server.BaseServer; import code.server.AppServer; -import code.server.MockServer; import code.client.Controllers.*; import javafx.scene.Scene; import code.server.IRecipeDb; +import code.server.mocking.MockServer; import java.io.FileReader; import java.io.IOException; diff --git a/app/src/main/java/code/server/ShareLinkMongoDb.java b/app/src/main/java/code/server/ShareLinkMongoDb.java deleted file mode 100644 index 071e7c4..0000000 --- a/app/src/main/java/code/server/ShareLinkMongoDb.java +++ /dev/null @@ -1,41 +0,0 @@ -package code.server; - -import java.util.List; - -import org.bson.Document; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mongodb.client.MongoCollection; - -public class ShareLinkMongoDb implements IShareLinkDb { - private MongoCollection shareLinkDocumentCollection; - - public ShareLinkMongoDb(MongoCollection shareLinkDocumentCollection) { - this.shareLinkDocumentCollection = shareLinkDocumentCollection; - } - - private ShareLink jsonToShareLink(Document shareLinkDocument) { - // Gson gson = new Gson(); - // ShareLink shareLink = gson.fromJson(shareLinkDocument.toJson(), ShareLink.class); - // JsonObject jsonObj = JsonParser.parseString(shareLinkDocument.toJson().toString()).getAsJsonObject(); - // String shareLinkId = jsonObj.getAsJsonObject("_id").get("$oid").getAsString(); - // shareLink.setId(shareLinkId); - // return shareLink; - return null; - } - - @Override - public List getRecipeIds(String receiverId) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getRecipeIds'"); - } - - @Override - public List getRecipeIds(String senderId, String receiverId) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getRecipeIds'"); - } - -} diff --git a/app/src/main/java/code/server/MockChatGPTRequestHandler.java b/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java similarity index 95% rename from app/src/main/java/code/server/MockChatGPTRequestHandler.java rename to app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java index edf2205..e37798b 100644 --- a/app/src/main/java/code/server/MockChatGPTRequestHandler.java +++ b/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java @@ -1,9 +1,11 @@ -package code.server; +package code.server.mocking; import java.io.IOException; import java.io.OutputStream; import com.sun.net.httpserver.*; +import code.server.TextToRecipe; + public class MockChatGPTRequestHandler extends TextToRecipe implements HttpHandler { private String sampleRecipe = """ diff --git a/app/src/main/java/code/server/MockDallERequestHandler.java b/app/src/main/java/code/server/mocking/MockDallERequestHandler.java similarity index 95% rename from app/src/main/java/code/server/MockDallERequestHandler.java rename to app/src/main/java/code/server/mocking/MockDallERequestHandler.java index b2d5d3a..a1ef361 100644 --- a/app/src/main/java/code/server/MockDallERequestHandler.java +++ b/app/src/main/java/code/server/mocking/MockDallERequestHandler.java @@ -1,6 +1,8 @@ -package code.server; +package code.server.mocking; import code.client.Model.AppConfig; +import code.server.RecipeToImage; + import com.sun.net.httpserver.*; import java.io.*; diff --git a/app/src/main/java/code/server/MockHttpConnection.java b/app/src/main/java/code/server/mocking/MockHttpConnection.java similarity index 94% rename from app/src/main/java/code/server/MockHttpConnection.java rename to app/src/main/java/code/server/mocking/MockHttpConnection.java index bea2bbc..b4e702a 100644 --- a/app/src/main/java/code/server/MockHttpConnection.java +++ b/app/src/main/java/code/server/mocking/MockHttpConnection.java @@ -1,9 +1,11 @@ -package code.server; +package code.server.mocking; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import code.server.IHttpConnection; + public class MockHttpConnection implements IHttpConnection { private int responseCode; private InputStream inputStream; diff --git a/app/src/main/java/code/server/MockServer.java b/app/src/main/java/code/server/mocking/MockServer.java similarity index 90% rename from app/src/main/java/code/server/MockServer.java rename to app/src/main/java/code/server/mocking/MockServer.java index eeaeb08..beba0af 100644 --- a/app/src/main/java/code/server/MockServer.java +++ b/app/src/main/java/code/server/mocking/MockServer.java @@ -1,4 +1,4 @@ -package code.server; +package code.server.mocking; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; @@ -7,6 +7,13 @@ import com.sun.net.httpserver.*; import code.client.Model.AppConfig; +import code.server.AccountMongoDB; +import code.server.AccountRequestHandler; +import code.server.BaseServer; +import code.server.IRecipeDb; +import code.server.RecipeMongoDb; +import code.server.RecipeRequestHandler; +import code.server.ShareRequestHandler; import java.io.IOException; import java.net.InetSocketAddress; diff --git a/app/src/main/java/code/server/MockWhisperRequestHandler.java b/app/src/main/java/code/server/mocking/MockWhisperRequestHandler.java similarity index 91% rename from app/src/main/java/code/server/MockWhisperRequestHandler.java rename to app/src/main/java/code/server/mocking/MockWhisperRequestHandler.java index a225dc8..f7953ce 100644 --- a/app/src/main/java/code/server/MockWhisperRequestHandler.java +++ b/app/src/main/java/code/server/mocking/MockWhisperRequestHandler.java @@ -1,6 +1,10 @@ -package code.server; +package code.server.mocking; import com.sun.net.httpserver.*; + +import code.server.IHttpConnection; +import code.server.VoiceToText; + import java.io.IOException; import java.io.OutputStream; import java.net.URI; diff --git a/app/src/test/java/code/RecipeToImageTest.java b/app/src/test/java/code/RecipeToImageTest.java index 2cbf815..37bc452 100644 --- a/app/src/test/java/code/RecipeToImageTest.java +++ b/app/src/test/java/code/RecipeToImageTest.java @@ -3,8 +3,8 @@ import org.junit.jupiter.api.Test; import code.server.BaseServer; -import code.server.MockServer; import code.server.Recipe; +import code.server.mocking.MockServer; import code.client.Model.AppConfig; import code.client.Model.Model; diff --git a/app/src/test/java/code/RefreshTest.java b/app/src/test/java/code/RefreshTest.java index ca78dbd..63ea44b 100644 --- a/app/src/test/java/code/RefreshTest.java +++ b/app/src/test/java/code/RefreshTest.java @@ -14,6 +14,7 @@ import code.client.View.*; import code.client.Controllers.*; import code.server.*; +import code.server.mocking.MockServer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; diff --git a/app/src/test/java/code/ServerConnectionTest.java b/app/src/test/java/code/ServerConnectionTest.java index 490363c..41c856f 100644 --- a/app/src/test/java/code/ServerConnectionTest.java +++ b/app/src/test/java/code/ServerConnectionTest.java @@ -7,7 +7,7 @@ import code.client.Model.AppConfig; import code.client.View.ServerConnection; import code.server.BaseServer; -import code.server.MockServer; +import code.server.mocking.MockServer; import java.io.IOException; import java.io.PrintStream; diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index 9d3567a..d8c665a 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -6,6 +6,7 @@ import code.client.Model.AppConfig; import code.client.Model.Model; import code.server.*; +import code.server.mocking.MockServer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; diff --git a/app/src/test/java/code/VoiceToTextTest.java b/app/src/test/java/code/VoiceToTextTest.java index 420518d..dc958c0 100644 --- a/app/src/test/java/code/VoiceToTextTest.java +++ b/app/src/test/java/code/VoiceToTextTest.java @@ -9,8 +9,8 @@ import code.client.Model.Model; import code.server.BaseServer; import code.server.IHttpConnection; -import code.server.MockHttpConnection; -import code.server.MockServer; +import code.server.mocking.MockHttpConnection; +import code.server.mocking.MockServer; import static org.junit.jupiter.api.Assertions.assertEquals; From 3d38f269cd07014b6713c18d8b405244a299ee21 Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 11:51:02 -0800 Subject: [PATCH 22/31] chore: remove unnecessary imports and methods --- .../code/client/Controllers/Controller.java | 25 ++++--------------- .../main/java/code/client/Model/Model.java | 3 --- .../java/code/client/Model/RecipeListDb.java | 1 - .../java/code/client/View/AppFrameHome.java | 5 +--- .../java/code/client/View/AppFrameMic.java | 3 +-- .../code/client/View/DetailsAppFrame.java | 5 ++-- .../java/code/client/View/Ingredients.java | 8 ------ .../main/java/code/client/View/LoginUI.java | 4 +-- .../main/java/code/client/View/MealType.java | 7 ------ .../java/code/client/View/RecipeListUI.java | 1 - .../code/server/AccountRequestHandler.java | 6 ----- app/src/main/java/code/server/AppServer.java | 1 - .../main/java/code/server/RecipeMongoDb.java | 19 ++++++-------- .../main/java/code/server/ShareRecipe.java | 22 ++++++++-------- .../java/code/server/ShareRequestHandler.java | 8 ------ .../java/code/server/mocking/MockServer.java | 1 - app/src/test/java/code/AddUserTest.java | 3 --- app/src/test/java/code/AudioRecorderTest.java | 6 ----- app/src/test/java/code/DeleteRecipeTest.java | 1 - app/src/test/java/code/RefreshTest.java | 11 -------- app/src/test/java/code/ShareRecipeTest.java | 3 --- app/src/test/java/code/TextToRecipeTest.java | 1 - 22 files changed, 30 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index e942559..ee2931f 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -3,7 +3,6 @@ import java.io.*; import java.net.URISyntaxException; import java.util.*; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import code.client.View.RecipeListUI; import code.client.View.RecipeUI; @@ -26,23 +25,15 @@ import javafx.util.Duration; import code.client.Model.*; import code.client.View.AppAlert; -import code.client.View.AppFrameHome; import code.client.View.AppFrameMic; import code.client.View.DetailsAppFrame; import code.server.Recipe; public class Controller { - // Index of newest to oldest in sorting drop down menu, index of breakfast in - // filtering drop down menu - private final int NEWEST_TO_OLDEST_INDEX = 0, BREAKFAST_INDEX = 0; - // Index of oldest to newest in sorting drop down menu, index of lunch in - // filtering drop down menu - private final int OLDEST_TO_NEWEST_INDEX = 1, LUNCH_INDEX = 1; - // Index of A to Z in sorting drop down menu, index of dinner in filtering drop - // down menu - private final int A_TO_Z_INDEX = 2, DINNER_INDEX = 2; - // Index of Z to A in sorting drop down menu, index of none in filtering drop - // down menu + // indices in sorting drop-down menu + private final int NEWEST_TO_OLDEST_INDEX = 0; + private final int OLDEST_TO_NEWEST_INDEX = 1; + private final int A_TO_Z_INDEX = 2; private final int Z_TO_A_INDEX = 3, NONE_INDEX = 3; private Account account; @@ -52,7 +43,6 @@ public class Controller { private IRecipeDb recipeDb; private RecipeCSVWriter recipeWriter; private RecipeCSVReader recipeReader; - private String title; private String defaultButtonStyle, onStyle, offStyle, blinkStyle; private String filter; @@ -61,7 +51,6 @@ public class Controller { private final AppAudioRecorder recorder = new AppAudioRecorder(); private String mealType; // stores the meal type specified by the user private String ingredients; // stores the ingredients listed out by the user - private ProgressBar progressBar; public Controller(View view, Model model) { @@ -96,10 +85,6 @@ public Controller(View view, Model model) { } } - public void setTitle(String title) { - this.title = title; - } - private void handleRecipePostButton(ActionEvent event) throws IOException { view.getDetailedView().getRefreshButton().setVisible(false); Recipe postedRecipe = view.getDetailedView().getDisplayedRecipe(); @@ -691,7 +676,7 @@ private void recordIngredients() { AppAlert.show("Input Error", "Please provide valid ingredients!"); ingredients = null; } else { - mic.getIngredBox().getIngredients().setText(ingredients); + mic.getIngrBox().getIngredients().setText(ingredients); } } catch (IOException exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index 70710a7..a9c47ec 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -14,9 +14,6 @@ import java.net.URI; import java.nio.file.*; import java.net.URLEncoder; -import com.mongodb.MongoException; -import com.mongodb.MongoSocketReadException; -import com.mongodb.MongoWriteException; public class Model { public String performAccountRequest(String method, String user, String password) { diff --git a/app/src/main/java/code/client/Model/RecipeListDb.java b/app/src/main/java/code/client/Model/RecipeListDb.java index a6a8db5..8f6faf9 100644 --- a/app/src/main/java/code/client/Model/RecipeListDb.java +++ b/app/src/main/java/code/client/Model/RecipeListDb.java @@ -75,7 +75,6 @@ public List getList() { @Override public List getList(String accountId) { - // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getList'"); } } diff --git a/app/src/main/java/code/client/View/AppFrameHome.java b/app/src/main/java/code/client/View/AppFrameHome.java index 099e039..c81aba6 100644 --- a/app/src/main/java/code/client/View/AppFrameHome.java +++ b/app/src/main/java/code/client/View/AppFrameHome.java @@ -48,7 +48,7 @@ class Header extends HBox { private MenuButton filterMenuButton, sortMenuButton; // Filtering criteria contained in the dropdown menu private MenuItem filterBreakfast, filterLunch, filterDinner, filterNone; - // Sorting crteria contained in the dropdown menu + // Sorting criteria contained in the dropdown menu private MenuItem sortNewToOld, sortOldToNew, sortAToZ, sortZToA; Header() { @@ -106,7 +106,6 @@ public class AppFrameHome extends BorderPane { private Header header; private Footer footer; private RecipeListUI recipeList; - private MenuButton filterMenuButton, sortMenuButton; private Button newButton, logOutButton; private StackPane stack; @@ -125,8 +124,6 @@ public class AppFrameHome extends BorderPane { this.setBottom(footer); newButton = footer.getNewButton(); - filterMenuButton = header.getFilterMenuButton(); - sortMenuButton = header.getSortMenuButton(); logOutButton = footer.getLogOutButton(); } diff --git a/app/src/main/java/code/client/View/AppFrameMic.java b/app/src/main/java/code/client/View/AppFrameMic.java index e79111f..7527d2f 100644 --- a/app/src/main/java/code/client/View/AppFrameMic.java +++ b/app/src/main/java/code/client/View/AppFrameMic.java @@ -1,6 +1,5 @@ package code.client.View; -import code.client.Model.*; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Pos; @@ -122,7 +121,7 @@ public MealType getMealBox() { return mealTypeSelection; } - public Ingredients getIngredBox() { + public Ingredients getIngrBox() { return ingredientsList; } diff --git a/app/src/main/java/code/client/View/DetailsAppFrame.java b/app/src/main/java/code/client/View/DetailsAppFrame.java index 51bbc46..e26a714 100644 --- a/app/src/main/java/code/client/View/DetailsAppFrame.java +++ b/app/src/main/java/code/client/View/DetailsAppFrame.java @@ -1,7 +1,6 @@ package code.client.View; import java.util.Date; -import code.client.Model.*; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.TextField; @@ -72,7 +71,7 @@ public Recipe getDisplayedRecipe() { String instructions = recipeInfo.getInstructionsField().getText(); String image = recipeInfo.getImageString(); - /// Use Trung's deformatting here. + // remove format String[] ingr = ingredients.split("\n"); String[] instr = instructions.split("\n"); @@ -118,7 +117,7 @@ private Recipe getMockedRecipe() { } public void updateDisplay() { - // Resets the UI everytime + // Resets the UI every time detailedUI.getChildren().clear(); VBox setupContainer = new VBox(); diff --git a/app/src/main/java/code/client/View/Ingredients.java b/app/src/main/java/code/client/View/Ingredients.java index 68df493..8813e50 100644 --- a/app/src/main/java/code/client/View/Ingredients.java +++ b/app/src/main/java/code/client/View/Ingredients.java @@ -1,20 +1,12 @@ package code.client.View; import code.client.Model.*; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Pos; import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.ArrayList; - import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.scene.paint.Color; -import javafx.scene.text.*; public class Ingredients extends GridPane { diff --git a/app/src/main/java/code/client/View/LoginUI.java b/app/src/main/java/code/client/View/LoginUI.java index b391ace..cf66bc4 100644 --- a/app/src/main/java/code/client/View/LoginUI.java +++ b/app/src/main/java/code/client/View/LoginUI.java @@ -19,8 +19,7 @@ public class LoginUI { - private boolean rememberLogin, accountSaved = false; - private Account savedAccount; + private boolean rememberLogin; private Hyperlink goToCreate; private Button loginButton; private TextField usernameField; @@ -75,7 +74,6 @@ public LoginUI() { public void setLoginCreds(Account account) { usernameField.setText(account.getUsername()); passwordField.setText(account.getPassword()); - savedAccount = account; } public GridPane getRoot() { diff --git a/app/src/main/java/code/client/View/MealType.java b/app/src/main/java/code/client/View/MealType.java index 25c306e..8e5322d 100644 --- a/app/src/main/java/code/client/View/MealType.java +++ b/app/src/main/java/code/client/View/MealType.java @@ -1,20 +1,13 @@ package code.client.View; import code.client.Model.*; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Pos; import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.ArrayList; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.scene.paint.Color; -import javafx.scene.text.*; public class MealType extends GridPane { private final Label prompt, recordingLabel; diff --git a/app/src/main/java/code/client/View/RecipeListUI.java b/app/src/main/java/code/client/View/RecipeListUI.java index 749750c..7b68e05 100644 --- a/app/src/main/java/code/client/View/RecipeListUI.java +++ b/app/src/main/java/code/client/View/RecipeListUI.java @@ -9,7 +9,6 @@ public class RecipeListUI extends VBox { private IRecipeDb recipeDb; - private String name; RecipeListUI() throws IOException { this.setSpacing(5); diff --git a/app/src/main/java/code/server/AccountRequestHandler.java b/app/src/main/java/code/server/AccountRequestHandler.java index c21d217..22cbc8b 100644 --- a/app/src/main/java/code/server/AccountRequestHandler.java +++ b/app/src/main/java/code/server/AccountRequestHandler.java @@ -50,9 +50,6 @@ public void handle(HttpExchange httpExchange) throws IOException { outStream.close(); } - /* - * TODO: Expects username and password - */ private String handleGet(HttpExchange httpExchange) throws IOException { String response = "Invalid GET request"; URI uri = httpExchange.getRequestURI(); @@ -80,9 +77,6 @@ private String handleGet(HttpExchange httpExchange) throws IOException { return response; } - /* - * TODO for accounts - */ private String handlePut(HttpExchange httpExchange) throws IOException { InputStream inStream = httpExchange.getRequestBody(); Scanner scanner = new Scanner(inStream); diff --git a/app/src/main/java/code/server/AppServer.java b/app/src/main/java/code/server/AppServer.java index ab94b0f..4d7b6f4 100644 --- a/app/src/main/java/code/server/AppServer.java +++ b/app/src/main/java/code/server/AppServer.java @@ -10,7 +10,6 @@ import java.io.IOException; import java.net.InetSocketAddress; -import java.net.URISyntaxException; import java.util.concurrent.*; import org.bson.Document; diff --git a/app/src/main/java/code/server/RecipeMongoDb.java b/app/src/main/java/code/server/RecipeMongoDb.java index fac7915..fe84f80 100644 --- a/app/src/main/java/code/server/RecipeMongoDb.java +++ b/app/src/main/java/code/server/RecipeMongoDb.java @@ -9,10 +9,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mongodb.client.MongoCollection; -import com.mongodb.client.model.Filters; import com.mongodb.client.model.UpdateOptions; -import com.mongodb.client.model.UpdateOptions; -import com.mongodb.client.model.Updates; import java.util.Arrays; import static com.mongodb.client.model.Filters.eq; @@ -81,13 +78,13 @@ public boolean add(Recipe recipe) { Bson updateInstr = set("instructions", Lists.newArrayList(recipe.getInstructionIterator())); Bson updateDate = set("date", recipe.getDate()); Bson updateImage = set("image", recipe.getImage()); - updates.addAll(Arrays.asList(updateUserId, - updateTitle, - updateMealTag, - updateIngr, - updateInstr, - updateDate, - updateImage)); + updates.addAll(Arrays.asList(updateUserId, + updateTitle, + updateMealTag, + updateIngr, + updateInstr, + updateDate, + updateImage)); UpdateOptions options = new UpdateOptions().upsert(true); recipeDocumentCollection.updateOne(filter, updates, options); return true; @@ -107,7 +104,7 @@ public Recipe find(String id) { @Override public boolean update(Recipe updatedRecipe) { - Bson filter = eq("_id", updatedRecipe.getId()); + eq("_id", updatedRecipe.getId()); return true; } diff --git a/app/src/main/java/code/server/ShareRecipe.java b/app/src/main/java/code/server/ShareRecipe.java index 47ce422..09d6b1a 100644 --- a/app/src/main/java/code/server/ShareRecipe.java +++ b/app/src/main/java/code/server/ShareRecipe.java @@ -42,16 +42,18 @@ private static String nonExistentRecipe() { return htmlBuilder.toString(); } - private static String getMockedRecipe() { - String title = "Fried Chicken and Egg Fried Rice"; - String mealType = "Breakfast"; - String ingredients = "2 chicken breasts, diced;;2 large eggs;;2 cups cooked rice;;2 tablespoons vegetable oil"; - String[] ingr = ingredients.split(";;"); - String instructions = "1. Heat the vegetable oil in a large pan over medium-high heat."; - String[] instr = instructions.split(";;"); - // return formatRecipe(title, ingr, instr); - return title; - } + // private static String getMockedRecipe() { + // String title = "Fried Chicken and Egg Fried Rice"; + // String mealType = "Breakfast"; + // String ingredients = "2 chicken breasts, diced;;2 large eggs;;2 cups cooked + // rice;;2 tablespoons vegetable oil"; + // String[] ingr = ingredients.split(";;"); + // String instructions = "1. Heat the vegetable oil in a large pan over + // medium-high heat."; + // String[] instr = instructions.split(";;"); + // // return formatRecipe(title, ingr, instr); + // return title; + // } private static String formatRecipe(Recipe recipe) { String title = recipe.getTitle() != null ? recipe.getTitle() : "Untitled Recipe"; diff --git a/app/src/main/java/code/server/ShareRequestHandler.java b/app/src/main/java/code/server/ShareRequestHandler.java index a52bd27..690cbbe 100644 --- a/app/src/main/java/code/server/ShareRequestHandler.java +++ b/app/src/main/java/code/server/ShareRequestHandler.java @@ -3,14 +3,7 @@ import java.io.IOException; import java.io.OutputStream; import java.net.URI; - -import org.bson.types.ObjectId; - -import java.util.Iterator; -import java.util.List; - import code.client.Model.*; - import com.sun.net.httpserver.*; public class ShareRequestHandler implements HttpHandler { @@ -26,7 +19,6 @@ public ShareRequestHandler(AccountMongoDB accountMongoDB, IRecipeDb recipeMongoD @Override public void handle(HttpExchange httpExchange) throws IOException { String response = "Request Received"; - String method = httpExchange.getRequestMethod(); URI uri = httpExchange.getRequestURI(); String query = uri.toString(); int usernameStart = query.indexOf(AppConfig.SHARE_PATH); diff --git a/app/src/main/java/code/server/mocking/MockServer.java b/app/src/main/java/code/server/mocking/MockServer.java index beba0af..d29c6df 100644 --- a/app/src/main/java/code/server/mocking/MockServer.java +++ b/app/src/main/java/code/server/mocking/MockServer.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.net.InetSocketAddress; -import java.net.URISyntaxException; import java.util.concurrent.*; import org.bson.Document; diff --git a/app/src/test/java/code/AddUserTest.java b/app/src/test/java/code/AddUserTest.java index 44118b5..09b5d52 100644 --- a/app/src/test/java/code/AddUserTest.java +++ b/app/src/test/java/code/AddUserTest.java @@ -4,13 +4,10 @@ import code.client.Model.AppConfig; import code.server.AccountMongoDB; import code.server.IAccountDb; -import code.client.Model.*; import static org.junit.jupiter.api.Assertions.*; import org.bson.Document; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.mongodb.client.MongoClient; diff --git a/app/src/test/java/code/AudioRecorderTest.java b/app/src/test/java/code/AudioRecorderTest.java index dc7aa7f..1856bc8 100644 --- a/app/src/test/java/code/AudioRecorderTest.java +++ b/app/src/test/java/code/AudioRecorderTest.java @@ -1,15 +1,9 @@ package code; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import code.server.*; import code.client.Model.*; import static org.junit.jupiter.api.Assertions.*; -import java.util.*; - import java.io.*; -import java.net.URISyntaxException; public class AudioRecorderTest { private BaseAudioRecorder audioRecorder; diff --git a/app/src/test/java/code/DeleteRecipeTest.java b/app/src/test/java/code/DeleteRecipeTest.java index d601ec9..0446d73 100644 --- a/app/src/test/java/code/DeleteRecipeTest.java +++ b/app/src/test/java/code/DeleteRecipeTest.java @@ -7,7 +7,6 @@ import java.util.List; import code.client.Model.*; -import code.server.IRecipeDb; import code.server.Recipe; /** diff --git a/app/src/test/java/code/RefreshTest.java b/app/src/test/java/code/RefreshTest.java index 63ea44b..3d06649 100644 --- a/app/src/test/java/code/RefreshTest.java +++ b/app/src/test/java/code/RefreshTest.java @@ -1,18 +1,8 @@ package code; -import code.client.Controllers.Controller; import code.client.Model.Model; -import code.client.View.DetailsAppFrame; -import code.client.View.View; -import javafx.scene.control.Button; -import javafx.stage.Stage; import org.junit.jupiter.api.Test; -import code.client.Model.Account; -import code.client.Model.AccountCSVReader; -import code.client.Model.AccountCSVWriter; import code.client.Model.AppConfig; -import code.client.View.*; -import code.client.Controllers.*; import code.server.*; import code.server.mocking.MockServer; @@ -21,7 +11,6 @@ import java.io.IOException; import java.net.URISyntaxException; -import java.util.*; public class RefreshTest { BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); diff --git a/app/src/test/java/code/ShareRecipeTest.java b/app/src/test/java/code/ShareRecipeTest.java index 5fe9b7b..5f42669 100644 --- a/app/src/test/java/code/ShareRecipeTest.java +++ b/app/src/test/java/code/ShareRecipeTest.java @@ -1,7 +1,5 @@ package code; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - import org.bson.Document; import org.junit.jupiter.api.Test; @@ -11,7 +9,6 @@ import com.mongodb.client.MongoDatabase; import static org.junit.jupiter.api.Assertions.*; import code.client.Model.AppConfig; -import code.server.Account; import code.server.AccountMongoDB; import code.server.RecipeMongoDb; import code.server.ShareRecipe; diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index d8c665a..f874bd2 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -9,7 +9,6 @@ import code.server.mocking.MockServer; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.IOException; From 7a75cbda7ac76994a523313dc04fad6978a88dca Mon Sep 17 00:00:00 2001 From: Samantha Prestrelski Date: Tue, 5 Dec 2023 12:51:03 -0800 Subject: [PATCH 23/31] ci: server unavailable tests --- .../mocking/MockWhisperRequestHandler.java | 2 +- .../test/java/code/EndToEndScenario2_2.java | 28 ++++++++++++++++++- app/src/test/java/code/VoiceToTextTest.java | 17 +++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/code/server/mocking/MockWhisperRequestHandler.java b/app/src/main/java/code/server/mocking/MockWhisperRequestHandler.java index f7953ce..1d0052e 100644 --- a/app/src/main/java/code/server/mocking/MockWhisperRequestHandler.java +++ b/app/src/main/java/code/server/mocking/MockWhisperRequestHandler.java @@ -24,7 +24,7 @@ public void handle(HttpExchange httpExchange) throws IOException { String query = uri.getRawQuery(); String type = query.substring(query.indexOf("=") + 1); - String response = ""; + String response = "Error"; if (type.equals("mealType")) { response = "Breakfast"; } else if (type.equals("ingredients")) { diff --git a/app/src/test/java/code/EndToEndScenario2_2.java b/app/src/test/java/code/EndToEndScenario2_2.java index 022b776..5371054 100644 --- a/app/src/test/java/code/EndToEndScenario2_2.java +++ b/app/src/test/java/code/EndToEndScenario2_2.java @@ -14,10 +14,36 @@ import code.client.View.*; import code.client.Controllers.*; import code.server.*; +import code.server.mocking.MockServer; +import java.net.ConnectException; +import java.net.MalformedURLException; public class EndToEndScenario2_2 { + BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + Model model = new Model(); + Format format = new Format(); + @Test - public void serverUnavailableTest() { + public void serverUnavailableTest() throws MalformedURLException, IOException { + String response = model.performAccountRequest("GET", "user", "password"); + assertTrue(response.contains("Error")); + + response = model.performRecipeRequest("GET", "recipe", "userId"); + assertTrue(response.contains("Error")); + + try { + model.performWhisperRequest("GET", "mealType"); + assert (false); + } catch (ConnectException e) { + assert (true); + } + + response = model.performChatGPTRequest("GET", "mealType", "ingredients"); + assertTrue(response.contains("Error")); + + response = model.performDallERequest("GET", "recipeTitle"); + assertTrue(response.contains("Error")); + } @Test diff --git a/app/src/test/java/code/VoiceToTextTest.java b/app/src/test/java/code/VoiceToTextTest.java index dc958c0..7fe4d67 100644 --- a/app/src/test/java/code/VoiceToTextTest.java +++ b/app/src/test/java/code/VoiceToTextTest.java @@ -1,6 +1,7 @@ package code; import java.io.*; +import java.net.MalformedURLException; import java.net.URISyntaxException; import org.junit.jupiter.api.Test; @@ -11,6 +12,7 @@ import code.server.IHttpConnection; import code.server.mocking.MockHttpConnection; import code.server.mocking.MockServer; +import java.net.ConnectException; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -32,9 +34,18 @@ void testSuccessfulProcessAudio() throws IOException, URISyntaxException { server.stop(); } - /* - * Unit test - */ + @Test + void testUnsuccessfulProcessAudio() throws MalformedURLException, IOException { + server.start(); + try { + String response = model.performWhisperRequest("GET", "error"); + assertEquals("Error", response); + } catch (ConnectException e) { + assert (false); + } + server.stop(); + } + @Test void testMockHttpCreation() throws IOException { IHttpConnection connection = new MockHttpConnection( From 10b14929d1716dc9db3d5723c7acfd4e0a518d4d Mon Sep 17 00:00:00 2001 From: Allen Keng Date: Tue, 5 Dec 2023 14:52:55 -0800 Subject: [PATCH 24/31] Separated Server from App. Slight UI adjustments. --- app/src/main/java/code/App.java | 6 ++--- app/src/main/java/code/Server.java | 26 +++++++++++++++++++ .../java/code/client/Model/AppConfig.java | 8 +++--- .../code/client/View/AccountCreationUI.java | 1 + .../java/code/client/View/AppFrameHome.java | 2 +- .../main/java/code/client/View/LoadingUI.java | 1 + .../main/java/code/client/View/LoginUI.java | 1 + .../main/java/code/client/View/OfflineUI.java | 7 ++++- .../java/code/client/View/RecipeListUI.java | 1 + .../code/client/View/ServerConnection.java | 11 +++++++- 10 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/code/Server.java diff --git a/app/src/main/java/code/App.java b/app/src/main/java/code/App.java index 8462006..20649d0 100644 --- a/app/src/main/java/code/App.java +++ b/app/src/main/java/code/App.java @@ -24,8 +24,8 @@ public class App extends Application { @Override public void start(Stage primaryStage) throws Exception { // initDb(); To use CSV file - initServer(); - server.start(); + // initServer(); + // server.start(); drawUI(primaryStage); } @@ -40,7 +40,7 @@ private void drawUI(Stage primaryStage) throws IOException, URISyntaxException { view.setScene(login); Controller controller = new Controller(view, model); - ServerConnection connection = new ServerConnection(server); + ServerConnection connection = new ServerConnection("localhost", 8100); if (connection.isOnline()) { // System.out.println("Server is online"); diff --git a/app/src/main/java/code/Server.java b/app/src/main/java/code/Server.java new file mode 100644 index 0000000..2fdb145 --- /dev/null +++ b/app/src/main/java/code/Server.java @@ -0,0 +1,26 @@ +package code; + +import java.io.IOException; + +import code.client.Model.AppConfig; +import code.server.AppServer; +import code.server.BaseServer; +import code.server.mocking.MockServer; + +public class Server { + private static BaseServer server; + + public static void main(String[] args) throws IOException { + initServer(); + server.start(); + } + + private static void initServer() throws IOException { + if (AppConfig.MOCKING_ON) { + server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + } else { + server = new AppServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + } + } + +} diff --git a/app/src/main/java/code/client/Model/AppConfig.java b/app/src/main/java/code/client/Model/AppConfig.java index 50d1559..e4b9463 100644 --- a/app/src/main/java/code/client/Model/AppConfig.java +++ b/app/src/main/java/code/client/Model/AppConfig.java @@ -12,10 +12,10 @@ public class AppConfig { public static final AudioFileFormat.Type AUDIO_TYPE = AudioFileFormat.Type.WAVE; public static final String API_KEY = "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"; // images - public static final String MICROPHONE_IMG_FILE = "app/src/main/java/code/client/View/microphone.png"; - public static final String OFFLINE_IMG_FILE = "app/src/main/java/code/client/View/cat.png"; - public static final String LOADING_IMG_FILE = "app/src/main/java/code/client/View/loading.png"; - public static final String RECIPE_IMG_FILE = "app/src/main/java/code/client/View/defaultRecipe.png"; + public static final String MICROPHONE_IMG_FILE = "app/src/main/java/code/client/View/images/microphone.png"; + public static final String OFFLINE_IMG_FILE = "app/src/main/java/code/client/View/images/cat.png"; + public static final String LOADING_IMG_FILE = "app/src/main/java/code/client/View/images/loading.png"; + public static final String RECIPE_IMG_FILE = "app/src/main/java/code/client/View/images/defaultRecipe.png"; // mongo public static final String MONGODB_CONN = "mongodb://trungluu:xGoGkkbozvWyiXyZ@ac-ajwebab-shard-00-00.lta1oi1.mongodb.net:27017,ac-ajwebab-shard-00-01.lta1oi1.mongodb.net:27017,ac-ajwebab-shard-00-02.lta1oi1.mongodb.net:27017/?ssl=true&replicaSet=atlas-3daxhg-shard-0&authSource=admin&retryWrites=true&w=majority"; public static final String MONGO_DB = "pantry_pal"; diff --git a/app/src/main/java/code/client/View/AccountCreationUI.java b/app/src/main/java/code/client/View/AccountCreationUI.java index 3aa52c6..18ee572 100644 --- a/app/src/main/java/code/client/View/AccountCreationUI.java +++ b/app/src/main/java/code/client/View/AccountCreationUI.java @@ -55,6 +55,7 @@ public class AccountCreationUI { flow.getChildren().addAll( new Text("Already have an account? "), goToLogin); grid.add(flow, 1, 5); + GridPane.setFillWidth(grid, true); } public GridPane getRoot() { diff --git a/app/src/main/java/code/client/View/AppFrameHome.java b/app/src/main/java/code/client/View/AppFrameHome.java index c81aba6..6e0c1b0 100644 --- a/app/src/main/java/code/client/View/AppFrameHome.java +++ b/app/src/main/java/code/client/View/AppFrameHome.java @@ -122,7 +122,7 @@ public class AppFrameHome extends BorderPane { this.setTop(header); this.setCenter(scroller); this.setBottom(footer); - + scroller.setFitToWidth(true); newButton = footer.getNewButton(); logOutButton = footer.getLogOutButton(); } diff --git a/app/src/main/java/code/client/View/LoadingUI.java b/app/src/main/java/code/client/View/LoadingUI.java index fcc7a62..59e38cf 100644 --- a/app/src/main/java/code/client/View/LoadingUI.java +++ b/app/src/main/java/code/client/View/LoadingUI.java @@ -30,6 +30,7 @@ public class LoadingUI extends HBox { loadingImg = new ImageView(new Image(file.toURI().toString())); loadingImg.setFitWidth(Region.USE_COMPUTED_SIZE); gridPane.add(loadingImg, 1, 2); + GridPane.setFillWidth(gridPane, true); getChildren().addAll(gridPane); } } diff --git a/app/src/main/java/code/client/View/LoginUI.java b/app/src/main/java/code/client/View/LoginUI.java index cf66bc4..9ea0c88 100644 --- a/app/src/main/java/code/client/View/LoginUI.java +++ b/app/src/main/java/code/client/View/LoginUI.java @@ -34,6 +34,7 @@ public LoginUI() { grid.setVgap(10); grid.setPadding(new Insets(25, 25, 25, 25)); grid.setStyle("-fx-background-color: #F0F8FF;"); + GridPane.setFillWidth(grid, true); Text titleText = new Text("Pantry Pal - Login"); titleText.setFont(Font.font("Arial", FontWeight.BOLD, 20)); diff --git a/app/src/main/java/code/client/View/OfflineUI.java b/app/src/main/java/code/client/View/OfflineUI.java index b5534fd..f8dab87 100644 --- a/app/src/main/java/code/client/View/OfflineUI.java +++ b/app/src/main/java/code/client/View/OfflineUI.java @@ -9,7 +9,7 @@ import code.client.Model.AppConfig; -public class OfflineUI extends HBox { +public class OfflineUI extends VBox { private final Label offlineLabel; private final ImageView offlineImg; @@ -25,7 +25,12 @@ public class OfflineUI extends HBox { // Show an image when the server is offline File file = new File(AppConfig.OFFLINE_IMG_FILE); offlineImg = new ImageView(new Image(file.toURI().toString())); + offlineImg.prefWidth(500); gridPane.add(offlineImg, 1, 2); getChildren().addAll(gridPane); + GridPane.setFillHeight(gridPane, true); + GridPane.setFillWidth(gridPane, true); + GridPane.setMargin(gridPane, new Insets(10, 10, 10, 10)); + this.setFillWidth(true); } } diff --git a/app/src/main/java/code/client/View/RecipeListUI.java b/app/src/main/java/code/client/View/RecipeListUI.java index 7b68e05..ea2b6f1 100644 --- a/app/src/main/java/code/client/View/RecipeListUI.java +++ b/app/src/main/java/code/client/View/RecipeListUI.java @@ -15,6 +15,7 @@ public class RecipeListUI extends VBox { this.setPrefSize(700, 600); this.setStyle("-fx-background-color: #F0F8FF;"); VBox.setVgrow(this, Priority.ALWAYS); + this.setFillWidth(true); } public IRecipeDb getRecipeDB() { diff --git a/app/src/main/java/code/client/View/ServerConnection.java b/app/src/main/java/code/client/View/ServerConnection.java index d940b19..be434f0 100644 --- a/app/src/main/java/code/client/View/ServerConnection.java +++ b/app/src/main/java/code/client/View/ServerConnection.java @@ -7,9 +7,18 @@ public class ServerConnection { private BaseServer server; + private String ipAddress; + private int port; public ServerConnection(BaseServer server) { this.server = server; + ipAddress = server.getHostName(); + port = server.getPort(); + } + + public ServerConnection(String ipAddress, int port) { + this.ipAddress = ipAddress; + this.port = port; } public BaseServer getServer() { @@ -22,7 +31,7 @@ public void setServer(BaseServer server) { public boolean isOnline() { try { - InetSocketAddress socketAddr = new InetSocketAddress(server.getHostName(), server.getPort()); + InetSocketAddress socketAddr = new InetSocketAddress(ipAddress, port); Socket socket = new Socket(); socket.connect(socketAddr, 500); socket.close(); From a7d8fe824075ab78b9c0eb9ec6a4d438264f9c79 Mon Sep 17 00:00:00 2001 From: Allen Keng Date: Tue, 5 Dec 2023 15:06:54 -0800 Subject: [PATCH 25/31] More UI resizing changes. --- .../main/java/code/client/View/AppFrameHome.java | 15 ++++++++------- app/src/main/java/code/client/View/LoadingUI.java | 3 ++- .../main/java/code/client/View/RecipeListUI.java | 4 +++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/code/client/View/AppFrameHome.java b/app/src/main/java/code/client/View/AppFrameHome.java index 6e0c1b0..f0728fd 100644 --- a/app/src/main/java/code/client/View/AppFrameHome.java +++ b/app/src/main/java/code/client/View/AppFrameHome.java @@ -74,14 +74,14 @@ class Header extends HBox { sortMenuButton.getItems().addAll(sortNewToOld, sortOldToNew, sortAToZ, sortZToA); - EventHandler event1 = new EventHandler() { - public void handle(ActionEvent e) { - System.out.println(((MenuItem) e.getSource()).getText() + " selected"); - } - }; + // EventHandler event1 = new EventHandler() { + // public void handle(ActionEvent e) { + // System.out.println(((MenuItem) e.getSource()).getText() + " selected"); + // } + // }; - sortMenuButton.getItems().get(2).setOnAction(event1); - sortMenuButton.getItems().get(3).setOnAction(event1); + // sortMenuButton.getItems().get(2).setOnAction(event1); + // sortMenuButton.getItems().get(3).setOnAction(event1); this.setPrefSize(620, 60); this.setStyle("-fx-background-color: #F0F8FF;"); @@ -125,6 +125,7 @@ public class AppFrameHome extends BorderPane { scroller.setFitToWidth(true); newButton = footer.getNewButton(); logOutButton = footer.getLogOutButton(); + BorderPane.setAlignment(this, Pos.CENTER); } public StackPane getRoot() { diff --git a/app/src/main/java/code/client/View/LoadingUI.java b/app/src/main/java/code/client/View/LoadingUI.java index 59e38cf..ccc12d9 100644 --- a/app/src/main/java/code/client/View/LoadingUI.java +++ b/app/src/main/java/code/client/View/LoadingUI.java @@ -10,7 +10,7 @@ import code.client.Model.AppConfig; -public class LoadingUI extends HBox { +public class LoadingUI extends VBox { private final Label loadingLabel; private final ImageView loadingImg; @@ -32,5 +32,6 @@ public class LoadingUI extends HBox { gridPane.add(loadingImg, 1, 2); GridPane.setFillWidth(gridPane, true); getChildren().addAll(gridPane); + this.setFillWidth(true); } } diff --git a/app/src/main/java/code/client/View/RecipeListUI.java b/app/src/main/java/code/client/View/RecipeListUI.java index ea2b6f1..03c1cc9 100644 --- a/app/src/main/java/code/client/View/RecipeListUI.java +++ b/app/src/main/java/code/client/View/RecipeListUI.java @@ -1,5 +1,6 @@ package code.client.View; +import javafx.geometry.Pos; import javafx.scene.layout.*; import java.io.*; import java.util.List; @@ -12,10 +13,11 @@ public class RecipeListUI extends VBox { RecipeListUI() throws IOException { this.setSpacing(5); - this.setPrefSize(700, 600); + // this.setPrefSize(700, 600); this.setStyle("-fx-background-color: #F0F8FF;"); VBox.setVgrow(this, Priority.ALWAYS); this.setFillWidth(true); + // this.setAlignment(Pos.CENTER); } public IRecipeDb getRecipeDB() { From e1652b71df5b0ce57ef4e4cdb13e8b32da48d816 Mon Sep 17 00:00:00 2001 From: AllKeng Date: Tue, 5 Dec 2023 18:59:03 -0800 Subject: [PATCH 26/31] Prepare for demo with incoming traffic + separate UI more from controller --- app/src/main/java/code/App.java | 4 +- .../code/client/Controllers/Controller.java | 198 ++++-------------- .../java/code/client/Model/AppConfig.java | 2 +- .../main/java/code/client/Model/Model.java | 6 +- .../java/code/client/View/AppFrameHome.java | 28 +-- .../java/code/client/View/RecipeListUI.java | 3 +- .../main/java/code/client/View/RecipeUI.java | 1 + app/src/main/java/code/client/View/View.java | 122 +++++++++++ app/src/main/java/code/server/AppServer.java | 2 +- .../java/code/server/mocking/MockServer.java | 2 +- 10 files changed, 184 insertions(+), 184 deletions(-) diff --git a/app/src/main/java/code/App.java b/app/src/main/java/code/App.java index 20649d0..181ddd2 100644 --- a/app/src/main/java/code/App.java +++ b/app/src/main/java/code/App.java @@ -38,18 +38,18 @@ private void drawUI(Stage primaryStage) throws IOException, URISyntaxException { Model model = new Model(); Scene login = new Scene(view.getLoginUI().getRoot()); view.setScene(login); - Controller controller = new Controller(view, model); + Controller controller; ServerConnection connection = new ServerConnection("localhost", 8100); if (connection.isOnline()) { + controller = new Controller(view, model); // System.out.println("Server is online"); controller.addListenersToList(); } else { // System.out.println("Server is offline"); view.goToOfflineUI(); } - primaryStage.setScene(login); primaryStage.setTitle(AppConfig.APP_NAME); primaryStage.setResizable(true); diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index ee2931f..32a7e54 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -3,26 +3,17 @@ import java.io.*; import java.net.URISyntaxException; import java.util.*; - import code.client.View.RecipeListUI; import code.client.View.RecipeUI; import code.client.View.View; import code.server.AccountRequestHandler; import code.server.IRecipeDb; - import java.net.URL; -import javafx.animation.*; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.event.ActionEvent; -import javafx.geometry.Pos; import javafx.scene.control.*; -import javafx.scene.input.Clipboard; -import javafx.scene.input.ClipboardContent; import javafx.scene.layout.GridPane; -import javafx.scene.paint.Color; -import javafx.scene.text.*; -import javafx.util.Duration; import code.client.Model.*; import code.client.View.AppAlert; import code.client.View.AppFrameMic; @@ -43,7 +34,6 @@ public class Controller { private IRecipeDb recipeDb; private RecipeCSVWriter recipeWriter; private RecipeCSVReader recipeReader; - private String defaultButtonStyle, onStyle, offStyle, blinkStyle; private String filter; // Audio Stuff @@ -57,10 +47,6 @@ public Controller(View view, Model model) { this.view = view; this.model = model; filter = "none"; - defaultButtonStyle = "-fx-font-style: italic; -fx-background-color: #FFFFFF; -fx-font-weight: bold; -fx-font: 11 arial;"; - onStyle = "-fx-font-style: italic; -fx-background-color: #90EE90; -fx-font-weight: bold; -fx-font: 11 arial;"; - offStyle = "-fx-font-style: italic; -fx-background-color: #FF7377; -fx-font-weight: bold; -fx-font: 11 arial;"; - blinkStyle = "-fx-background-color: #00FFFF; -fx-border-width: 0;"; this.view.getAppFrameHome().setNewRecipeButtonAction(event -> { try { @@ -85,17 +71,28 @@ public Controller(View view, Model model) { } } + private void handleNewButton(ActionEvent event) throws URISyntaxException, IOException { + view.goToAudioCapture(); + AppFrameMic mic = this.view.getAppFrameMic(); + mic.setGoToDetailedButtonAction(this::handleDetailedViewFromNewRecipeButton); + mic.setGoToHomeButtonAction(this::handleHomeButton); + mic.setRecordIngredientsButtonAction(this::handleRecordIngredients); + mic.setRecordMealTypeButtonAction(event1 -> { + try { + handleRecordMealType(event1); + } catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } + }); + + } + private void handleRecipePostButton(ActionEvent event) throws IOException { view.getDetailedView().getRefreshButton().setVisible(false); Recipe postedRecipe = view.getDetailedView().getDisplayedRecipe(); Date currTime = new Date(); postedRecipe.setDate(currTime.getTime()); - - Button saveButtonFromDetailed = view.getDetailedView().getSaveButton(); - saveButtonFromDetailed.setStyle(blinkStyle); - PauseTransition pause = new PauseTransition(Duration.seconds(2.5)); - pause.setOnFinished(f -> saveButtonFromDetailed.setStyle(defaultButtonStyle)); - pause.play(); + view.callSaveAnimation(); Writer writer = new StringWriter(); recipeWriter = new RecipeCSVWriter(writer); @@ -111,6 +108,17 @@ private void handleRecipePostButton(ActionEvent event) throws IOException { } } + private void handleLogOutOutButton(ActionEvent event) { + clearCredentials(); + view.goToLoginUI(); + view.getLoginUI().getUsernameTextField().clear(); + view.getLoginUI().getPasswordField().clear(); + } + + private void handleHomeButton(ActionEvent event) { + goToRecipeList(); + } + private void goToRecipeList() { getUserRecipeList(); displayUserRecipes(); @@ -118,8 +126,8 @@ private void goToRecipeList() { addListenersToList(); MenuButton filterMenuButton = this.view.getAppFrameHome().getFilterMenuButton(); MenuButton sortMenuButton = this.view.getAppFrameHome().getSortMenuButton(); - setActiveState(filterMenuButton, 9); - setActiveState(sortMenuButton, 9); + view.setActiveState(filterMenuButton, 9); + view.setActiveState(sortMenuButton, 9); RecipeListUI recipeListUI = this.view.getAppFrameHome().getRecipeList(); RecipeSorter recipeSorter = new RecipeSorter(recipeListUI.getRecipeDB().getList()); @@ -148,22 +156,6 @@ private void displayUserRecipes() { recipeListUI.update(filter); } - private void handleNewButton(ActionEvent event) throws URISyntaxException, IOException { - view.goToAudioCapture(); - AppFrameMic mic = this.view.getAppFrameMic(); - mic.setGoToDetailedButtonAction(this::handleDetailedViewFromNewRecipeButton); - mic.setGoToHomeButtonAction(this::handleHomeButton); - mic.setRecordIngredientsButtonAction(this::handleRecordIngredients); - mic.setRecordMealTypeButtonAction(event1 -> { - try { - handleRecordMealType(event1); - } catch (IOException | URISyntaxException e) { - e.printStackTrace(); - } - }); - - } - private void addFilterListeners() { MenuButton filterMenuButton = this.view.getAppFrameHome().getFilterMenuButton(); ObservableList filterMenuItems = filterMenuButton.getItems(); @@ -174,7 +166,7 @@ private void addFilterListeners() { int index = i; filterMenuItems.get(index).setOnAction(e -> { filter = filterTypes[index]; - setActiveState(filterMenuButton, index); + view.setActiveState(filterMenuButton, index); this.view.getAppFrameHome().updateDisplay(filterTypes[index]); addListenersToList(); }); @@ -204,32 +196,11 @@ private void setSortAction(ObservableList sortMenuItems, int index, Ru } private void sortList(MenuButton sortMenuButton, int index) { - setActiveState(sortMenuButton, index); + view.setActiveState(sortMenuButton, index); this.view.getAppFrameHome().updateDisplay(filter); addListenersToList(); } - private void setActiveState(MenuButton items, int index) { - for (int i = 0; i < NONE_INDEX + 1; i++) { - if (i == index) { - items.getItems().get(i).setStyle("-fx-background-color: #90EE90"); - } else { - items.getItems().get(i).setStyle("-fx-background-color: transparent;"); - } - } - } - - private void handleLogOutOutButton(ActionEvent event) { - clearCredentials(); - view.goToLoginUI(); - view.getLoginUI().getUsernameTextField().clear(); - view.getLoginUI().getPasswordField().clear(); - } - - private void handleHomeButton(ActionEvent event) { - goToRecipeList(); - } - public void addListenersToList() { addSortingListener(); addFilterListeners(); @@ -248,7 +219,7 @@ public void addListenersToList() { currRecipe.getDetailsButton().setOnAction(e -> { view.goToDetailedView(currRecipe.getRecipe(), true); view.getDetailedView().getRecipeDetailsUI().setEditable(false); - changeEditButtonColor(view.getDetailedView().getEditButton()); + view.changeEditButtonColor(view.getDetailedView().getEditButton()); handleDetailedViewListeners(); }); } @@ -271,12 +242,6 @@ private void handleDetailedViewFromNewRecipeButton(ActionEvent event) { handleDetailedViewListeners(); }); thread.start(); - AppFrameMic mic = view.getAppFrameMic(); - mic.getRecordingIngredientsLabel() - .setText("Processing mealType and ingredients. Please wait."); - mic.getRecordingIngredientsLabel() - .setStyle("-fx-font-weight: bold; -fx-font: 20 arial;"); - mic.getRecordingIngredientsLabel().setVisible(true); } catch (Exception exception) { AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); exception.printStackTrace(); @@ -323,7 +288,7 @@ private void handleDetailedViewListeners() { private void handleEditButton(ActionEvent event) { Button edit = view.getDetailedView().getEditButton(); view.getDetailedView().getRecipeDetailsUI().setEditable(); - changeEditButtonColor(edit); + view.changeEditButtonColor(edit); } private void handleDeleteButton(ActionEvent event) throws IOException { @@ -365,37 +330,7 @@ private void handleShareButton(ActionEvent event) { } private void showShareRecipe(Hyperlink textArea) { - String styleAlert = "-fx-background-color: #F1FFCB; -fx-font-weight: bold; -fx-font: 14 arial"; - - GridPane gridPane = new GridPane(); - gridPane.setMaxWidth(Double.MAX_VALUE); - gridPane.add(textArea, 0, 0); - gridPane.setStyle(styleAlert); - gridPane.setPrefSize(220, 220); - gridPane.setAlignment(Pos.TOP_CENTER); - textArea.setTextAlignment(TextAlignment.CENTER); - Button copyButton = new Button("Copy to Clipboard"); - copyButton.setOnAction(event -> { - Clipboard clipboard = Clipboard.getSystemClipboard(); - ClipboardContent content = new ClipboardContent(); - content.putString(textArea.getText()); - clipboard.setContent(content); - }); - gridPane.add(copyButton, 0, 3); - - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle("Share this recipe!"); - alert.setHeaderText("Share this recipe with a friend!"); - alert.getDialogPane().setContent(gridPane); - alert.showAndWait(); - } - - private void changeEditButtonColor(Button edit) { - if (view.getDetailedView().getRecipeDetailsUI().isEditable()) { - edit.setStyle(onStyle); - } else { - edit.setStyle(offStyle); - } + view.displaySharedRecipeUI(textArea); } private void handleGoToCreateLogin(ActionEvent event) { @@ -434,14 +369,14 @@ private void handleRefreshButton(ActionEvent event) throws URISyntaxException, I } } - //////////////////////////////////////// +/////////////////////////////// ACCOUNT MANAGEMENT /////////////////////////////////// private void handleCreateAcc(ActionEvent event) { GridPane grid = view.getAccountCreationUI().getRoot(); String username = view.getAccountCreationUI().getUsernameTextField().getText(); String password = view.getAccountCreationUI().getPasswordField().getText(); if (username.isEmpty() || password.isEmpty()) { - showErrorPane(grid, "Error. Please provide a username and password."); + view.showErrorPane(grid, "Error. Please provide a username and password."); return; } @@ -455,46 +390,18 @@ private void handleCreateAcc(ActionEvent event) { if (response.contains("Offline")) { view.goToOfflineUI(); } else { - showSuccessPane(grid); + view.showSuccessPane(grid); view.goToLoginUI(); } } else { view.goToCreateAcc(); Platform.runLater( - () -> showErrorPane(grid, "Error. This username is already taken. Please choose another one.")); + () -> view.showErrorPane(grid, "Error. This username is already taken. Please choose another one.")); } }); thread.start(); } - private void showErrorPane(GridPane grid, String errorMessage) { - Text errorText = new Text(errorMessage); - errorText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); - errorText.setFill(Color.RED); - - grid.add(errorText, 1, 6); - - // Fade away after 5 seconds - Timeline timeline = new Timeline( - new KeyFrame(Duration.seconds(0), new KeyValue(errorText.opacityProperty(), 1.0)), - new KeyFrame(Duration.seconds(5), new KeyValue(errorText.opacityProperty(), 0.0))); - timeline.play(); - } - - private void showSuccessPane(GridPane grid) { - Text successText = new Text("Successfully created an account!\nPlease login to access it."); - successText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); - successText.setFill(Color.GREEN); - - grid.add(successText, 1, 6); - - // Fade away after 5 seconds - Timeline timeline = new Timeline( - new KeyFrame(Duration.seconds(0), new KeyValue(successText.opacityProperty(), 1.0)), - new KeyFrame(Duration.seconds(5), new KeyValue(successText.opacityProperty(), 0.0))); - timeline.play(); - } - private boolean isUsernameTaken(String username, String password) { // Check if the username is already taken String response = model.performAccountRequest("GET", username, password); @@ -505,7 +412,6 @@ private boolean isUsernameTaken(String username, String password) { return (!response.equals("Username is not found")); } - //////////////////////////////////////// private void handleLoginButton(ActionEvent event) { String username = view.getLoginUI().getUsernameTextField().getText(); String password = view.getLoginUI().getPasswordField().getText(); @@ -513,7 +419,7 @@ private void handleLoginButton(ActionEvent event) { // Perform login logic here if (username.isEmpty() || password.isEmpty()) { // Display an error message if username or password is empty - showErrorPane(grid, "Error. Please provide a username and password."); + view.showErrorPane(grid, "Error. Please provide a username and password."); return; } @@ -532,7 +438,7 @@ private void handleLoginButton(ActionEvent event) { } else { view.goToLoginUI(); Platform.runLater( - () -> showLoginSuccessPane(grid, false)); + () -> view.showLoginSuccessPane(grid, false)); } }); thread.start(); @@ -579,25 +485,6 @@ private void loadCredentials() { } } - private void showLoginSuccessPane(GridPane grid, boolean loginSuccessful) { - Text successText; - if (loginSuccessful) { - successText = new Text("Login successful! Welcome to Pantry Pal."); - successText.setFill(Color.GREEN); - } else { - successText = new Text("Account does not exist. Please try again."); - successText.setFill(Color.RED); - } - - successText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); - grid.add(successText, 1, 6); - - Timeline timeline = new Timeline( - new KeyFrame(Duration.seconds(0), new KeyValue(successText.opacityProperty(), 1.0)), - new KeyFrame(Duration.seconds(5), new KeyValue(successText.opacityProperty(), 0.0))); - timeline.play(); - } - private boolean performLogin(String username, String password) { // Will add logic for failed login later String response = model.performAccountRequest("GET", username, password); @@ -617,8 +504,9 @@ private boolean performLogin(String username, String password) { account = new Account(accountId, username, password); return true; } + /////////////////////////////// ACCOUNT MANAGEMENT /////////////////////////////////// - /////////////////////////////// AUDIOMANAGEMENT/////////////////////////////////// + /////////////////////////////// AUDIO MANAGEMENT/////////////////////////////////// public void handleRecordMealType(ActionEvent event) throws IOException, URISyntaxException { recordMealType(); } diff --git a/app/src/main/java/code/client/Model/AppConfig.java b/app/src/main/java/code/client/Model/AppConfig.java index e4b9463..e3b00ac 100644 --- a/app/src/main/java/code/client/Model/AppConfig.java +++ b/app/src/main/java/code/client/Model/AppConfig.java @@ -7,7 +7,7 @@ public class AppConfig { public static final String RECIPE_CSV_FILE = "recipes.csv"; public static final String CREDENTIALS_CSV_FILE = "userCredentials.csv"; // API - public static final boolean MOCKING_ON = false; + public static final boolean MOCKING_ON = true; public static final String AUDIO_FILE = "recording.wav"; public static final AudioFileFormat.Type AUDIO_TYPE = AudioFileFormat.Type.WAVE; public static final String API_KEY = "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"; diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index a9c47ec..c3c0bb5 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -70,9 +70,13 @@ public String performRecipeRequest(String method, String recipe, String userId) } BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String tempResponse = ""; String line; while ((line = in.readLine()) != null) { - response += line + "\n"; + tempResponse += line + "\n"; + } + if( !(tempResponse.toLowerCase().contains("error")) ) { + response = tempResponse; } in.close(); } catch (Exception ex) { diff --git a/app/src/main/java/code/client/View/AppFrameHome.java b/app/src/main/java/code/client/View/AppFrameHome.java index f0728fd..8999386 100644 --- a/app/src/main/java/code/client/View/AppFrameHome.java +++ b/app/src/main/java/code/client/View/AppFrameHome.java @@ -2,7 +2,7 @@ package code.client.View; import javafx.scene.control.*; - +import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.layout.*; import javafx.scene.text.*; import java.io.*; @@ -18,7 +18,7 @@ class Footer extends HBox { this.setPrefSize(620, 60); this.setStyle("-fx-background-color: #F0F8FF;"); this.setSpacing(15); - + this.setAlignment(Pos.CENTER); String defaultButtonStyle = "-fx-font-style: italic; -fx-background-color: #FFFFFF; -fx-font-weight: bold; -fx-font: 11 arial;"; newButton = new Button("New Recipe"); @@ -74,15 +74,6 @@ class Header extends HBox { sortMenuButton.getItems().addAll(sortNewToOld, sortOldToNew, sortAToZ, sortZToA); - // EventHandler event1 = new EventHandler() { - // public void handle(ActionEvent e) { - // System.out.println(((MenuItem) e.getSource()).getText() + " selected"); - // } - // }; - - // sortMenuButton.getItems().get(2).setOnAction(event1); - // sortMenuButton.getItems().get(3).setOnAction(event1); - this.setPrefSize(620, 60); this.setStyle("-fx-background-color: #F0F8FF;"); @@ -107,32 +98,27 @@ public class AppFrameHome extends BorderPane { private Footer footer; private RecipeListUI recipeList; private Button newButton, logOutButton; - private StackPane stack; AppFrameHome() throws IOException { - stack = new StackPane(); header = new Header(); recipeList = new RecipeListUI(); footer = new Footer(); ScrollPane scroller = new ScrollPane(recipeList); - scroller.setFitToWidth(true); - scroller.setFitToHeight(true); - + scroller.setMaxSize(400,400); + scroller.setVbarPolicy(ScrollBarPolicy.ALWAYS); this.setTop(header); this.setCenter(scroller); this.setBottom(footer); - scroller.setFitToWidth(true); newButton = footer.getNewButton(); logOutButton = footer.getLogOutButton(); BorderPane.setAlignment(this, Pos.CENTER); + } - public StackPane getRoot() { - stack.getChildren().clear(); - stack.getChildren().add(this); + public BorderPane getRoot() { this.updateDisplay("none"); - return stack; + return this; } public void updateDisplay(String filter) { diff --git a/app/src/main/java/code/client/View/RecipeListUI.java b/app/src/main/java/code/client/View/RecipeListUI.java index 03c1cc9..547b88c 100644 --- a/app/src/main/java/code/client/View/RecipeListUI.java +++ b/app/src/main/java/code/client/View/RecipeListUI.java @@ -15,8 +15,7 @@ public class RecipeListUI extends VBox { this.setSpacing(5); // this.setPrefSize(700, 600); this.setStyle("-fx-background-color: #F0F8FF;"); - VBox.setVgrow(this, Priority.ALWAYS); - this.setFillWidth(true); + //VBox.setVgrow(this, Priority.ALWAYS); // this.setAlignment(Pos.CENTER); } diff --git a/app/src/main/java/code/client/View/RecipeUI.java b/app/src/main/java/code/client/View/RecipeUI.java index d923a3c..a704fa3 100644 --- a/app/src/main/java/code/client/View/RecipeUI.java +++ b/app/src/main/java/code/client/View/RecipeUI.java @@ -40,6 +40,7 @@ public class RecipeUI extends HBox { MealTagStyler.styleTags(recipe, mealType); this.getChildren().add(style); this.setPrefSize(50, 50); + this.setMinSize(50, 50); } public Recipe getRecipe() { diff --git a/app/src/main/java/code/client/View/View.java b/app/src/main/java/code/client/View/View.java index 459db44..2fd6fd6 100644 --- a/app/src/main/java/code/client/View/View.java +++ b/app/src/main/java/code/client/View/View.java @@ -4,8 +4,26 @@ import java.net.URISyntaxException; import code.server.Recipe; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.PauseTransition; +import javafx.animation.Timeline; +import javafx.geometry.Pos; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.MenuButton; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.layout.GridPane; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; +import javafx.util.Duration; public class View { private AppFrameHome home; @@ -16,6 +34,7 @@ public class View { private Scene mainScene; private OfflineUI offlineScreen; private LoadingUI loadingUI; + private String blinkStyle, defaultButtonStyle, onStyle, offStyle; public View() throws IOException, URISyntaxException { offlineScreen = new OfflineUI(); @@ -25,6 +44,9 @@ public View() throws IOException, URISyntaxException { detailedRecipe = new DetailsAppFrame(); createAcc = new AccountCreationUI(); loadingUI = new LoadingUI(); + defaultButtonStyle = "-fx-font-style: italic; -fx-background-color: #FFFFFF; -fx-font-weight: bold; -fx-font: 11 arial;"; + onStyle = "-fx-font-style: italic; -fx-background-color: #90EE90; -fx-font-weight: bold; -fx-font: 11 arial;"; + offStyle = "-fx-font-style: italic; -fx-background-color: #FF7377; -fx-font-weight: bold; -fx-font: 11 arial;"; } public Parent getMainScene() { @@ -91,4 +113,104 @@ public LoginUI getLoginUI() { public AccountCreationUI getAccountCreationUI() { return createAcc; } + + public void callSaveAnimation() { + blinkStyle = "-fx-background-color: #00FFFF; -fx-border-width: 0;"; + Button saveButtonFromDetailed = detailedRecipe.getSaveButton(); + saveButtonFromDetailed.setStyle(blinkStyle); + PauseTransition pause = new PauseTransition(Duration.seconds(2.5)); + pause.setOnFinished(f -> saveButtonFromDetailed.setStyle(defaultButtonStyle)); + pause.play(); + } + + public void setActiveState(MenuButton items, int index) { + for (int i = 0; i < 4; i++) { + if (i == index) { + items.getItems().get(i).setStyle("-fx-background-color: #90EE90"); + } else { + items.getItems().get(i).setStyle("-fx-background-color: transparent;"); + } + } + } + + public void displaySharedRecipeUI(Hyperlink textArea) { + String styleAlert = "-fx-background-color: #F1FFCB; -fx-font-weight: bold; -fx-font: 14 arial"; + + GridPane gridPane = new GridPane(); + gridPane.setMaxWidth(Double.MAX_VALUE); + gridPane.add(textArea, 0, 0); + gridPane.setStyle(styleAlert); + gridPane.setPrefSize(220, 220); + gridPane.setAlignment(Pos.TOP_CENTER); + textArea.setTextAlignment(TextAlignment.CENTER); + Button copyButton = new Button("Copy to Clipboard"); + copyButton.setOnAction(event -> { + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + content.putString(textArea.getText()); + clipboard.setContent(content); + }); + gridPane.add(copyButton, 0, 3); + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Share this recipe!"); + alert.setHeaderText("Share this recipe with a friend!"); + alert.getDialogPane().setContent(gridPane); + alert.showAndWait(); + } + + public void changeEditButtonColor(Button edit) { + if (detailedRecipe.getRecipeDetailsUI().isEditable()) { + edit.setStyle(onStyle); + } else { + edit.setStyle(offStyle); + } + } + + public void showLoginSuccessPane(GridPane grid, boolean loginSuccessful) { + Text successText; + if (loginSuccessful) { + successText = new Text("Login successful! Welcome to Pantry Pal."); + successText.setFill(Color.GREEN); + } else { + successText = new Text("Account does not exist. Please try again."); + successText.setFill(Color.RED); + } + + successText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + grid.add(successText, 1, 6); + + Timeline timeline = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(successText.opacityProperty(), 1.0)), + new KeyFrame(Duration.seconds(5), new KeyValue(successText.opacityProperty(), 0.0))); + timeline.play(); + } + + public void showErrorPane(GridPane grid, String errorMessage) { + Text errorText = new Text(errorMessage); + errorText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + errorText.setFill(Color.RED); + + grid.add(errorText, 1, 6); + + // Fade away after 5 seconds + Timeline timeline = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(errorText.opacityProperty(), 1.0)), + new KeyFrame(Duration.seconds(5), new KeyValue(errorText.opacityProperty(), 0.0))); + timeline.play(); + } + + public void showSuccessPane(GridPane grid) { + Text successText = new Text("Successfully created an account!\nPlease login to access it."); + successText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + successText.setFill(Color.GREEN); + + grid.add(successText, 1, 6); + + // Fade away after 5 seconds + Timeline timeline = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(successText.opacityProperty(), 1.0)), + new KeyFrame(Duration.seconds(5), new KeyValue(successText.opacityProperty(), 0.0))); + timeline.play(); + } } diff --git a/app/src/main/java/code/server/AppServer.java b/app/src/main/java/code/server/AppServer.java index 4d7b6f4..a4eaed4 100644 --- a/app/src/main/java/code/server/AppServer.java +++ b/app/src/main/java/code/server/AppServer.java @@ -38,7 +38,7 @@ public void start() throws IOException { // create a map to store data // create a server httpServer = HttpServer.create( - new InetSocketAddress(hostName, port), + new InetSocketAddress("0.0.0.0", port), 0); // create the context to map urls httpServer.createContext(AppConfig.RECIPE_PATH, new RecipeRequestHandler(recipeDb)); diff --git a/app/src/main/java/code/server/mocking/MockServer.java b/app/src/main/java/code/server/mocking/MockServer.java index d29c6df..e9f815a 100644 --- a/app/src/main/java/code/server/mocking/MockServer.java +++ b/app/src/main/java/code/server/mocking/MockServer.java @@ -45,7 +45,7 @@ public void start() throws IOException { // create a map to store data // create a server httpServer = HttpServer.create( - new InetSocketAddress(hostName, port), + new InetSocketAddress("0.0.0.0", port), 0); // create the context to map urls httpServer.createContext(AppConfig.RECIPE_PATH, new RecipeRequestHandler(recipeDb)); From 101e75c7220c38fe1c9c4643bdaf8f1dd3785670 Mon Sep 17 00:00:00 2001 From: AllKeng Date: Tue, 5 Dec 2023 19:42:36 -0800 Subject: [PATCH 27/31] Made Ip address adjustments to allow for proper url usage --- app/src/main/java/code/App.java | 4 ++-- app/src/main/java/code/client/Model/AppConfig.java | 2 +- app/src/main/java/code/client/View/AppFrameHome.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/code/App.java b/app/src/main/java/code/App.java index 181ddd2..c9c6b1c 100644 --- a/app/src/main/java/code/App.java +++ b/app/src/main/java/code/App.java @@ -39,8 +39,8 @@ private void drawUI(Stage primaryStage) throws IOException, URISyntaxException { Scene login = new Scene(view.getLoginUI().getRoot()); view.setScene(login); Controller controller; - - ServerConnection connection = new ServerConnection("localhost", 8100); + //123 + ServerConnection connection = new ServerConnection(AppConfig.SERVER_HOST, 8100); if (connection.isOnline()) { controller = new Controller(view, model); diff --git a/app/src/main/java/code/client/Model/AppConfig.java b/app/src/main/java/code/client/Model/AppConfig.java index e3b00ac..811632e 100644 --- a/app/src/main/java/code/client/Model/AppConfig.java +++ b/app/src/main/java/code/client/Model/AppConfig.java @@ -32,5 +32,5 @@ public class AppConfig { public static final String WHISPER_PATH = "/whisper"; // sharing public static final String SHARE_PATH = "/recipes/"; - public static final String SHARE_LINK = "http://localhost:8100/recipes/"; + public static final String SHARE_LINK = "http://" + SERVER_HOST + ":8100/recipes/"; } diff --git a/app/src/main/java/code/client/View/AppFrameHome.java b/app/src/main/java/code/client/View/AppFrameHome.java index 8999386..bdfaca9 100644 --- a/app/src/main/java/code/client/View/AppFrameHome.java +++ b/app/src/main/java/code/client/View/AppFrameHome.java @@ -30,7 +30,7 @@ class Footer extends HBox { grid.add(newButton, 11, 0); grid.setHgap(20); this.getChildren().add(grid); - this.setAlignment(Pos.CENTER_LEFT); + this.setAlignment(Pos.CENTER); } public Button getNewButton() { From 20c22d63d355fed1835f9e3b44432283f9e00aa5 Mon Sep 17 00:00:00 2001 From: AllKeng Date: Tue, 5 Dec 2023 19:54:06 -0800 Subject: [PATCH 28/31] Fixed ScrollPane --- .../java/code/client/View/AppFrameHome.java | 27 ++++++++++--------- .../java/code/client/View/RecipeListUI.java | 4 +-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/code/client/View/AppFrameHome.java b/app/src/main/java/code/client/View/AppFrameHome.java index bdfaca9..5088752 100644 --- a/app/src/main/java/code/client/View/AppFrameHome.java +++ b/app/src/main/java/code/client/View/AppFrameHome.java @@ -93,37 +93,40 @@ public MenuButton getFilterMenuButton() { } } -public class AppFrameHome extends BorderPane { +public class AppFrameHome extends VBox { private Header header; private Footer footer; private RecipeListUI recipeList; + private ScrollPane scroller; private Button newButton, logOutButton; AppFrameHome() throws IOException { - header = new Header(); recipeList = new RecipeListUI(); footer = new Footer(); - ScrollPane scroller = new ScrollPane(recipeList); - scroller.setMaxSize(400,400); - scroller.setVbarPolicy(ScrollBarPolicy.ALWAYS); - this.setTop(header); - this.setCenter(scroller); - this.setBottom(footer); + scroller = new ScrollPane(recipeList); + scroller.setFitToWidth(true); + scroller.setFitToHeight(true); + + this.getChildren().addAll(header,scroller,footer); + // this.setTop(header); + // this.setCenter(scroller); + // this.setBottom(footer); newButton = footer.getNewButton(); logOutButton = footer.getLogOutButton(); - BorderPane.setAlignment(this, Pos.CENTER); - } - public BorderPane getRoot() { + public VBox getRoot() { + // stack.getChildren().clear(); + // stack.getChildren().add(this); this.updateDisplay("none"); return this; } public void updateDisplay(String filter) { recipeList.update(filter); - this.setCenter(recipeList); + this.getChildren().clear(); + this.getChildren().addAll(header,scroller,footer); } public void setNewRecipeButtonAction(EventHandler eventHandler) { diff --git a/app/src/main/java/code/client/View/RecipeListUI.java b/app/src/main/java/code/client/View/RecipeListUI.java index 547b88c..c652b46 100644 --- a/app/src/main/java/code/client/View/RecipeListUI.java +++ b/app/src/main/java/code/client/View/RecipeListUI.java @@ -13,10 +13,10 @@ public class RecipeListUI extends VBox { RecipeListUI() throws IOException { this.setSpacing(5); - // this.setPrefSize(700, 600); + this.setPrefSize(700, 500); this.setStyle("-fx-background-color: #F0F8FF;"); //VBox.setVgrow(this, Priority.ALWAYS); - // this.setAlignment(Pos.CENTER); + this.setAlignment(Pos.CENTER); } public IRecipeDb getRecipeDB() { From 9add16d0af1332443496459fc247168b77710c7f Mon Sep 17 00:00:00 2001 From: AllKeng Date: Tue, 5 Dec 2023 22:41:49 -0800 Subject: [PATCH 29/31] Testing things. Co-authored-by: kjanderson1 Co-authored-by: ChristophianSulaiman Co-authored-by: dashluu Co-authored-by: Timoji Co-authored-by: Samantha Prestrelski --- app/src/main/java/code/App.java | 2 +- .../code/client/Controllers/Controller.java | 74 ++++-- .../java/code/client/Controllers/Format.java | 3 + .../java/code/client/Model/AppConfig.java | 2 +- .../{View => Model}/ServerConnection.java | 2 +- .../java/code/client/View/AppFrameHome.java | 11 +- .../code/client/View/DetailsAppFrame.java | 14 +- .../java/code/client/View/Ingredients.java | 1 - .../code/client/View/RecipeDetailsUI.java | 2 + .../java/code/client/View/RecipeListUI.java | 2 +- .../main/java/code/client/View/RecipeUI.java | 1 + .../main/java/code/server/RecipeMongoDb.java | 5 +- .../java/code/server/ShareRequestHandler.java | 6 +- .../main/java/code/server/TextToRecipe.java | 1 - .../code/server/WhisperRequestHandler.java | 2 + .../mocking/MockChatGPTRequestHandler.java | 33 ++- .../mocking/MockDallERequestHandler.java | 4 + app/src/test/java/code/CreateRecipeTest.java | 1 - .../test/java/code/EndToEndScenario2_1.java | 102 +++++-- .../test/java/code/EndToEndScenario2_2.java | 251 ++++++++++++++++-- app/src/test/java/code/RecipeToImageTest.java | 2 +- app/src/test/java/code/RefreshTest.java | 2 +- .../test/java/code/ServerConnectionTest.java | 6 +- app/src/test/java/code/TextToRecipeTest.java | 2 +- app/src/test/java/code/VoiceToTextTest.java | 2 +- 25 files changed, 435 insertions(+), 98 deletions(-) rename app/src/main/java/code/client/{View => Model}/ServerConnection.java (97%) diff --git a/app/src/main/java/code/App.java b/app/src/main/java/code/App.java index c9c6b1c..9dfaa6c 100644 --- a/app/src/main/java/code/App.java +++ b/app/src/main/java/code/App.java @@ -40,7 +40,7 @@ private void drawUI(Stage primaryStage) throws IOException, URISyntaxException { view.setScene(login); Controller controller; //123 - ServerConnection connection = new ServerConnection(AppConfig.SERVER_HOST, 8100); + ServerConnection connection = new ServerConnection(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); if (connection.isOnline()) { controller = new Controller(view, model); diff --git a/app/src/main/java/code/client/Controllers/Controller.java b/app/src/main/java/code/client/Controllers/Controller.java index 32a7e54..658df90 100644 --- a/app/src/main/java/code/client/Controllers/Controller.java +++ b/app/src/main/java/code/client/Controllers/Controller.java @@ -67,7 +67,7 @@ public Controller(View view, Model model) { loadCredentials(); if (account != null) { this.view.getLoginUI().setLoginCreds(account); - goToRecipeList(); + goToRecipeList(true); } } @@ -90,8 +90,7 @@ private void handleNewButton(ActionEvent event) throws URISyntaxException, IOExc private void handleRecipePostButton(ActionEvent event) throws IOException { view.getDetailedView().getRefreshButton().setVisible(false); Recipe postedRecipe = view.getDetailedView().getDisplayedRecipe(); - Date currTime = new Date(); - postedRecipe.setDate(currTime.getTime()); + view.callSaveAnimation(); Writer writer = new StringWriter(); @@ -99,13 +98,22 @@ private void handleRecipePostButton(ActionEvent event) throws IOException { recipeWriter.writeRecipe(postedRecipe); String recipe = writer.toString(); - - String response = model.performRecipeRequest("POST", recipe, null); - if (response.contains("Offline")) { - AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); - } else if (response.contains("Error")) { - AppAlert.show("Error", "Something went wrong. Please check your inputs and try again."); - } + Thread thread = new Thread(() -> { + String response = model.performRecipeRequest("POST", recipe, null); + // Changes UI to Detailed Recipe Screen + Platform.runLater( + () -> { + + if (response.contains("Offline")) { + AppAlert.show("Connection Error", "Something went wrong. Please check your connection and try again."); + } else if (response.contains("Error")) { + AppAlert.show("Error", "Something went wrong. Please check your inputs and try again."); + } + getUserRecipeList(); + displayUserRecipes(); + }); + }); + thread.start(); } private void handleLogOutOutButton(ActionEvent event) { @@ -116,12 +124,14 @@ private void handleLogOutOutButton(ActionEvent event) { } private void handleHomeButton(ActionEvent event) { - goToRecipeList(); + goToRecipeList(false); } - private void goToRecipeList() { - getUserRecipeList(); - displayUserRecipes(); + private void goToRecipeList(boolean afterChanges) { + if(afterChanges) { + getUserRecipeList(); + displayUserRecipes(); + } view.goToRecipeList(); addListenersToList(); MenuButton filterMenuButton = this.view.getAppFrameHome().getFilterMenuButton(); @@ -237,9 +247,13 @@ private void handleDetailedViewFromNewRecipeButton(ActionEvent event) { chatGPTrecipe.setImage(model.performDallERequest("GET", chatGPTrecipe.getTitle())); // Changes UI to Detailed Recipe Screen - view.goToDetailedView(chatGPTrecipe, false); - view.getDetailedView().getRecipeDetailsUI().setEditable(false); - handleDetailedViewListeners(); + Platform.runLater( + () -> { + view.goToDetailedView(chatGPTrecipe, false); + view.getDetailedView().getRecipeDetailsUI().setEditable(false); + handleDetailedViewListeners(); + }); + }); thread.start(); } catch (Exception exception) { @@ -269,7 +283,6 @@ private void handleDetailedViewListeners() { detailedView.setDeleteButtonAction(event -> { try { handleDeleteButton(event); - goToRecipeList(); } catch (IOException e) { e.printStackTrace(); } @@ -306,9 +319,17 @@ private void deleteGivenRecipe(Recipe recipe) throws IOException { String recipeStr = writer.toString(); System.out.println("Deleting id: " + recipe.getId()); - model.performRecipeRequest("DELETE", recipeStr, null); - this.view.getAppFrameHome().updateDisplay(filter); - goToRecipeList(); + Thread thread = new Thread(() -> { + model.performRecipeRequest("DELETE", recipeStr, null); + Platform.runLater( + () -> { + goToRecipeList(true); + this.view.getAppFrameHome().updateDisplay(filter); + addListenersToList(); + }); + }); + + thread.start(); } private void handleShareButton(ActionEvent event) { @@ -355,9 +376,12 @@ private void handleRefreshButton(ActionEvent event) throws URISyntaxException, I chatGPTrecipe.getTitle())); // Changes UI to Detailed Recipe Screen - view.goToDetailedView(chatGPTrecipe, false); - view.getDetailedView().getRecipeDetailsUI().setEditable(false); - handleDetailedViewListeners(); + Platform.runLater( + () -> { + view.goToDetailedView(chatGPTrecipe, false); + view.getDetailedView().getRecipeDetailsUI().setEditable(false); + handleDetailedViewListeners(); + }); }); thread.start(); } catch (Exception exception) { @@ -429,7 +453,7 @@ private void handleLoginButton(ActionEvent event) { if (view.getMainScene().equals(view.getOfflineUI())) { } else if (loginSuccessful) { Platform.runLater( - () -> goToRecipeList()); + () -> goToRecipeList(true)); if (!view.getLoginUI().getRememberLogin()) { clearCredentials(); } else { diff --git a/app/src/main/java/code/client/Controllers/Format.java b/app/src/main/java/code/client/Controllers/Format.java index c070928..0e4dfab 100644 --- a/app/src/main/java/code/client/Controllers/Format.java +++ b/app/src/main/java/code/client/Controllers/Format.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; public class Format { public String buildPrompt(String mealType, String ingredients) { @@ -38,6 +39,8 @@ public Recipe mapResponseToRecipe(String mealType, String responseText) { title = title.replaceAll("Title:", ""); } Recipe recipe = new Recipe(title.trim(), mealType); + Date now = new Date(); + recipe.setDate(now.getTime()); // Parse recipe's ingredients String ingredient; boolean parse = false; diff --git a/app/src/main/java/code/client/Model/AppConfig.java b/app/src/main/java/code/client/Model/AppConfig.java index 811632e..f5eb263 100644 --- a/app/src/main/java/code/client/Model/AppConfig.java +++ b/app/src/main/java/code/client/Model/AppConfig.java @@ -22,7 +22,7 @@ public class AppConfig { public static final String MONGO_RECIPE_COLLECTION = "recipes"; public static final String MONGO_USER_COLLECTION = "users"; // server - public static final String SERVER_HOST = "localhost"; + public static final String SERVER_HOST = "192.168.1.123"; public static final int SERVER_PORT = 8100; public static final String SERVER_URL = "http://" + SERVER_HOST + ":" + SERVER_PORT; public static final String RECIPE_PATH = "/recipe"; diff --git a/app/src/main/java/code/client/View/ServerConnection.java b/app/src/main/java/code/client/Model/ServerConnection.java similarity index 97% rename from app/src/main/java/code/client/View/ServerConnection.java rename to app/src/main/java/code/client/Model/ServerConnection.java index be434f0..340e758 100644 --- a/app/src/main/java/code/client/View/ServerConnection.java +++ b/app/src/main/java/code/client/Model/ServerConnection.java @@ -1,4 +1,4 @@ -package code.client.View; +package code.client.Model; import java.net.InetSocketAddress; import java.net.Socket; diff --git a/app/src/main/java/code/client/View/AppFrameHome.java b/app/src/main/java/code/client/View/AppFrameHome.java index 5088752..7e1ebb2 100644 --- a/app/src/main/java/code/client/View/AppFrameHome.java +++ b/app/src/main/java/code/client/View/AppFrameHome.java @@ -15,8 +15,8 @@ class Footer extends HBox { Footer() { GridPane grid = new GridPane(); - this.setPrefSize(620, 60); this.setStyle("-fx-background-color: #F0F8FF;"); + this.setPrefSize(620, 60); this.setSpacing(15); this.setAlignment(Pos.CENTER); String defaultButtonStyle = "-fx-font-style: italic; -fx-background-color: #FFFFFF; -fx-font-weight: bold; -fx-font: 11 arial;"; @@ -74,8 +74,9 @@ class Header extends HBox { sortMenuButton.getItems().addAll(sortNewToOld, sortOldToNew, sortAToZ, sortZToA); - this.setPrefSize(620, 60); + this.setStyle("-fx-background-color: #F0F8FF;"); + this.setPrefSize(620, 60); Text titleText = new Text("Recipe List"); titleText.setStyle("-fx-font-weight: bold; -fx-font-size: 20;"); @@ -107,11 +108,15 @@ public class AppFrameHome extends VBox { scroller = new ScrollPane(recipeList); scroller.setFitToWidth(true); scroller.setFitToHeight(true); - + this.setStyle("-fx-background-color: #F0F8FF;"); + this.setAlignment(Pos.CENTER); this.getChildren().addAll(header,scroller,footer); + this.setSpacing(30); // this.setTop(header); // this.setCenter(scroller); // this.setBottom(footer); + header.setAlignment(Pos.TOP_CENTER); + footer.setAlignment(Pos.BOTTOM_CENTER); newButton = footer.getNewButton(); logOutButton = footer.getLogOutButton(); } diff --git a/app/src/main/java/code/client/View/DetailsAppFrame.java b/app/src/main/java/code/client/View/DetailsAppFrame.java index e26a714..1c81808 100644 --- a/app/src/main/java/code/client/View/DetailsAppFrame.java +++ b/app/src/main/java/code/client/View/DetailsAppFrame.java @@ -78,13 +78,13 @@ public Recipe getDisplayedRecipe() { RecipeBuilder builder = new RecipeBuilder(currentRecipe.getAccountId(), title); builder.setMealTag(currentRecipe.getMealTag()); builder.setId(currentRecipe.getId()); - - if (isOldRecipe) { - builder.setDate(currentRecipe.getDate()); - } else { - Date currDate = new Date(); - builder.setDate(currDate.getTime()); - } + builder.setDate(currentRecipe.getDate()); + // if (isOldRecipe) { + + // } else { + // Date currDate = new Date(); + // builder.setDate(currDate.getTime()); + // } Recipe edit = builder.buildRecipe(); for (String ingredient : ingr) { diff --git a/app/src/main/java/code/client/View/Ingredients.java b/app/src/main/java/code/client/View/Ingredients.java index 8813e50..481fbbc 100644 --- a/app/src/main/java/code/client/View/Ingredients.java +++ b/app/src/main/java/code/client/View/Ingredients.java @@ -20,7 +20,6 @@ public class Ingredients extends GridPane { // Set the preferred vertical and horizontal gaps this.setVgap(20); this.setHgap(20); - // Get a picture of a microphone for the voice recording button File file = new File(AppConfig.MICROPHONE_IMG_FILE); microphone = new ImageView(new Image(file.toURI().toString())); diff --git a/app/src/main/java/code/client/View/RecipeDetailsUI.java b/app/src/main/java/code/client/View/RecipeDetailsUI.java index 975ea0d..e34904e 100644 --- a/app/src/main/java/code/client/View/RecipeDetailsUI.java +++ b/app/src/main/java/code/client/View/RecipeDetailsUI.java @@ -1,5 +1,6 @@ package code.client.View; +import javafx.geometry.Pos; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.image.Image; @@ -56,6 +57,7 @@ public RecipeDetailsUI(Recipe recipe) { // getChildren().add(titleTextField); getChildren().add(ingredientTextArea); getChildren().add(instructionTextArea); + this.setAlignment(Pos.CENTER); } public TextField getTitleField() { diff --git a/app/src/main/java/code/client/View/RecipeListUI.java b/app/src/main/java/code/client/View/RecipeListUI.java index c652b46..83bef14 100644 --- a/app/src/main/java/code/client/View/RecipeListUI.java +++ b/app/src/main/java/code/client/View/RecipeListUI.java @@ -13,8 +13,8 @@ public class RecipeListUI extends VBox { RecipeListUI() throws IOException { this.setSpacing(5); - this.setPrefSize(700, 500); this.setStyle("-fx-background-color: #F0F8FF;"); + this.setPrefSize(700, 455); //VBox.setVgrow(this, Priority.ALWAYS); this.setAlignment(Pos.CENTER); } diff --git a/app/src/main/java/code/client/View/RecipeUI.java b/app/src/main/java/code/client/View/RecipeUI.java index a704fa3..8aa69ee 100644 --- a/app/src/main/java/code/client/View/RecipeUI.java +++ b/app/src/main/java/code/client/View/RecipeUI.java @@ -41,6 +41,7 @@ public class RecipeUI extends HBox { this.getChildren().add(style); this.setPrefSize(50, 50); this.setMinSize(50, 50); + this.setAlignment(Pos.CENTER); } public Recipe getRecipe() { diff --git a/app/src/main/java/code/server/RecipeMongoDb.java b/app/src/main/java/code/server/RecipeMongoDb.java index fe84f80..ef5f868 100644 --- a/app/src/main/java/code/server/RecipeMongoDb.java +++ b/app/src/main/java/code/server/RecipeMongoDb.java @@ -78,7 +78,8 @@ public boolean add(Recipe recipe) { Bson updateInstr = set("instructions", Lists.newArrayList(recipe.getInstructionIterator())); Bson updateDate = set("date", recipe.getDate()); Bson updateImage = set("image", recipe.getImage()); - updates.addAll(Arrays.asList(updateUserId, + updates.addAll(Arrays.asList( + updateUserId, updateTitle, updateMealTag, updateIngr, @@ -129,4 +130,6 @@ public int size() { return (int) recipeDocumentCollection.countDocuments(); } + + } diff --git a/app/src/main/java/code/server/ShareRequestHandler.java b/app/src/main/java/code/server/ShareRequestHandler.java index 690cbbe..abc9549 100644 --- a/app/src/main/java/code/server/ShareRequestHandler.java +++ b/app/src/main/java/code/server/ShareRequestHandler.java @@ -28,9 +28,9 @@ public void handle(HttpExchange httpExchange) throws IOException { // Format: localhost:8100/recipes/username/recipeID - System.out.println("\n" + uri.toString()); - System.out.println(username); - System.out.println(recipeID); + // System.out.println("\n" + uri.toString()); + // System.out.println(username); + // System.out.println(recipeID); response = ShareRecipe.getSharedRecipe(accountMongoDB, recipeMongoDb, username, recipeID); // Sending back response to the client httpExchange.sendResponseHeaders(200, response.length()); diff --git a/app/src/main/java/code/server/TextToRecipe.java b/app/src/main/java/code/server/TextToRecipe.java index cdfbae2..ad96d74 100644 --- a/app/src/main/java/code/server/TextToRecipe.java +++ b/app/src/main/java/code/server/TextToRecipe.java @@ -7,5 +7,4 @@ public abstract class TextToRecipe { public abstract void handle(HttpExchange httpExchange) throws IOException; public abstract void setSampleRecipe(String recipe); - } diff --git a/app/src/main/java/code/server/WhisperRequestHandler.java b/app/src/main/java/code/server/WhisperRequestHandler.java index 50f3840..b7053fe 100644 --- a/app/src/main/java/code/server/WhisperRequestHandler.java +++ b/app/src/main/java/code/server/WhisperRequestHandler.java @@ -69,6 +69,7 @@ public void handle(HttpExchange httpExchange) throws IOException { response = "Error"; } } + // Sending back response to the client httpExchange.sendResponseHeaders(200, response.length()); OutputStream outStream = httpExchange.getResponseBody(); @@ -83,6 +84,7 @@ private static String readLine(InputStream multipartInStream, String lineSeparat while (multipartInStream.available() > 0) { int nextByte = multipartInStream.read(); + if (nextByte < -1) { throw new IOException("Reached end of stream while reading the current line!"); } diff --git a/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java b/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java index e37798b..cc2823d 100644 --- a/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java +++ b/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.io.OutputStream; +import java.net.URI; + import com.sun.net.httpserver.*; import code.server.TextToRecipe; @@ -28,23 +30,42 @@ public void setSampleRecipe(String recipeText) { @Override public void handle(HttpExchange httpExchange) throws IOException { String method = httpExchange.getRequestMethod(); - if (method.equals("GET2")) { - sampleRecipe = """ + URI uri = httpExchange.getRequestURI(); + String query = uri.getRawQuery(); + String response = "Error"; + try { + String value = query.substring(query.indexOf("=") + 1); + String[] typeIngredients = value.split("::"); + String mealType = typeIngredients[0]; + String ingredients = typeIngredients[1]; + + if (mealType.equals("Breakfast")) { + response = sampleRecipe; + } + } catch (IndexOutOfBoundsException e) { + e.printStackTrace(); + } + if (method.equals("PUT")) { + response = """ Fried Chicken and Egg Fried Rice breakfast Ingredients: - - 2 chicken breasts, diced - 2 large eggs - 2 cups cooked rice - 2 tablespoons vegetable oil - 2 tablespoons soy sauce - 1 teaspoon sesame oil - - Salt and pepper to taste"""; + - Salt and pepper to taste + Instructions: + 1. Crack 2 eggs into bowl. + 2. Have a shrimp fry the rice. + 3. Enjoy! + """; } - httpExchange.sendResponseHeaders(200, sampleRecipe.length()); + httpExchange.sendResponseHeaders(200, response.length()); OutputStream outStream = httpExchange.getResponseBody(); - outStream.write(sampleRecipe.getBytes()); + outStream.write(response.getBytes()); outStream.close(); } diff --git a/app/src/main/java/code/server/mocking/MockDallERequestHandler.java b/app/src/main/java/code/server/mocking/MockDallERequestHandler.java index a1ef361..61ca03f 100644 --- a/app/src/main/java/code/server/mocking/MockDallERequestHandler.java +++ b/app/src/main/java/code/server/mocking/MockDallERequestHandler.java @@ -21,7 +21,11 @@ public void handle(HttpExchange httpExchange) throws IOException { try { String recipeTitle = query.substring(query.indexOf("=") + 1); response = getResponse(recipeTitle); + if (recipeTitle.equals("recipeTitle")) { + response = "Error"; + } } catch (Exception e) { + response = "Error"; System.out.println("An erroneous request"); e.printStackTrace(); } diff --git a/app/src/test/java/code/CreateRecipeTest.java b/app/src/test/java/code/CreateRecipeTest.java index 7b95995..8e025f7 100644 --- a/app/src/test/java/code/CreateRecipeTest.java +++ b/app/src/test/java/code/CreateRecipeTest.java @@ -31,5 +31,4 @@ public void testCreateRecipe() { """; assertEquals(parsedResponse, recipeString); } - } diff --git a/app/src/test/java/code/EndToEndScenario2_1.java b/app/src/test/java/code/EndToEndScenario2_1.java index 9e62ce5..b1b134a 100644 --- a/app/src/test/java/code/EndToEndScenario2_1.java +++ b/app/src/test/java/code/EndToEndScenario2_1.java @@ -8,22 +8,25 @@ import com.mongodb.client.MongoDatabase; import org.bson.Document; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeAll; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import java.net.URISyntaxException; +import java.io.IOException; import java.io.FileWriter; import java.io.FileReader; -import java.io.IOException; import java.util.List; -import code.server.Account; -import code.client.Model.AccountCSVReader; +import code.client.Model.*; import code.client.Model.AccountCSVWriter; import code.client.Model.AppConfig; -import code.client.View.*; -import code.client.Controllers.*; +import code.client.Model.Model; import code.server.*; +import code.server.Account; +import code.server.mocking.MockServer; +import code.server.mocking.MockChatGPTRequestHandler; /** * This test file covers the End-to-End Scenario in which Chef Caitlyn: @@ -35,14 +38,23 @@ */ public class EndToEndScenario2_1 { private static Account account; // Account used in the following tests + private static BaseServer server; // Mock server used in the following tests + private static Model model; // Model used in the following tests @BeforeAll - public static void setUp() { + public static void setUp() throws IOException { + // Initialize an account for Chef Caitlyn account = new Account("Chef", "Caitlyn"); + // Initialize a mocked server that PantryPal will "use" + server = new MockServer("localhost", AppConfig.SERVER_PORT); + // Initialize a helper model object + model = new Model(); + // Start up the server before Chef Caitlyn opens up the app + server.start(); } /** - * Test that Chef Caitlyn was able to successfully create an account on MongoDB + * Test that Chef Caitlyn was able to successfully create an account on MongoDB. */ @Test public void createAccountTest() { @@ -60,7 +72,7 @@ public void createAccountTest() { /** * Test that Chef Caitlyn's user credentials were successfully saved onto her - * device + * device. */ @Test public void automaticLoginTest() throws IOException { @@ -80,13 +92,27 @@ public void automaticLoginTest() throws IOException { } /** - * Test that Chef Caitlyn can successfully create a recipe + * Test that Chef Caitlyn can successfully generate a recipe after logging in. */ @Test public void createRecipeTest() { - - // RecipeBuilder builder = new RecipeBuilder(); - + // Perform a mock ChatGPT request for Chef Caitlyn's fried chicken recipe + String mealType = "breakfast"; + String ingredients = "chicken, eggs"; + String initialResponse = model.performChatGPTRequest("GET", mealType, ingredients); + String expectedResponse = """ + Fried Chicken + breakfast + Ingredients: + - 2 chicken breasts, diced + - 2 eggs + Instructions: + 1. Crack 2 eggs into bowl. + 2. Add chicken into bowl and then fry. + 3. Enjoy! + """; + // Check that the recipe was created successfully from the ChatGPT response + assertEquals(expectedResponse, initialResponse); } /** @@ -96,28 +122,68 @@ public void createRecipeTest() { @Test public void refreshRecipeTest() { + /* START OF COPIED TEST CONTENT */ + + String mealType = "breakfast"; + String ingredients = "chicken, eggs"; + String initialResponse = model.performChatGPTRequest("GET", mealType, ingredients); + String expectedResponse = """ + Fried Chicken + breakfast + Ingredients: + - 2 chicken breasts, diced + - 2 eggs + Instructions: + 1. Crack 2 eggs into bowl. + 2. Add chicken into bowl and then fry. + 3. Enjoy! + """; + assertEquals(expectedResponse, initialResponse); + + /* END OF COPIED TEST CONTENT */ + + // Mock a recipe refresh on the recipe created in the previous test + String refreshResponse = model.performChatGPTRequest("PUT", mealType, ingredients); + // Check that the recipe body is no longer the same + assertNotEquals(initialResponse, refreshResponse); } /** - * Test that Chef Caitlyn can successfully save a recipe + * Test that Chef Caitlyn can successfully save a recipe to MongoDB. */ @Test public void saveRecipeTest() { - RecipeBuilder builder = new RecipeBuilder(account.getId(), "Caitlyn's Lunch"); - builder.setMealTag("lunch"); + // Build a recipe based on the mocked refreshed recipe from the previous test + RecipeBuilder builder = new RecipeBuilder(account.getId(), "Fried Chicken and Egg Fried Rice"); + builder.setMealTag("breakfast"); Recipe recipe = builder.buildRecipe(); + // Add the ingredients of the "refreshed" recipe + recipe.addIngredient("- 2 chicken breasts, diced"); + recipe.addIngredient("- 2 large eggs"); + recipe.addIngredient("- 2 cups cooked rice"); + recipe.addIngredient("- 2 tablespoons vegetable oil"); + recipe.addIngredient("- 2 tablespoons soy sauce"); + recipe.addIngredient("- 1 teaspoon sesame oil"); + recipe.addIngredient("- Salt and pepper to taste"); + // Add the instructions of the "refreshed" recipe + recipe.addInstruction("1. Crack 2 eggs into bowl."); + recipe.addInstruction("2. Have a shrimp fry the rice."); + recipe.addInstruction("3. Enjoy!"); try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); RecipeMongoDb recipeDB = new RecipeMongoDb(recipeCollection); recipeDB.add(recipe); Recipe receivedRecipe = recipeDB.find(recipe.getId()); + // Check that the recipe was saved to the MongoDB assertTrue(receivedRecipe != null); + // Check that the recipe was saved to Chef Caitlyn's account assertTrue(receivedRecipe.getAccountId().contains(account.getId())); recipeDB.remove(recipe.getId()); } catch (Exception e) { e.printStackTrace(); } + // Stop the server once Chef Caitlyn is done using the app + server.stop(); } - } \ No newline at end of file diff --git a/app/src/test/java/code/EndToEndScenario2_2.java b/app/src/test/java/code/EndToEndScenario2_2.java index 5371054..0ef90e2 100644 --- a/app/src/test/java/code/EndToEndScenario2_2.java +++ b/app/src/test/java/code/EndToEndScenario2_2.java @@ -1,44 +1,129 @@ package code; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.BeforeAll; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.FileWriter; import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; -import java.util.List; +import java.net.ConnectException; +import java.net.MalformedURLException; import java.util.ArrayList; +import java.util.List; + +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; -import code.client.Model.*; -import code.client.View.*; -import code.client.Controllers.*; -import code.server.*; +import code.client.Controllers.Format; +import code.client.Model.AccountCSVReader; +import code.client.Model.AccountCSVWriter; +import code.client.Model.AppConfig; +import code.client.Model.Model; +import code.client.Model.RecipeSorter; +import code.server.Account; +import code.server.AccountMongoDB; +import code.server.BaseServer; +import code.server.Recipe; +import code.server.RecipeBuilder; +import code.server.RecipeMongoDb; +import code.server.ShareRecipe; import code.server.mocking.MockServer; -import java.net.ConnectException; -import java.net.MalformedURLException; +/** + * This test file covers the End-to-End Scenario in which Chef Caitlyn: + * 1. Encounters a glitch where the server is temporarily unavailable + * 2. Sucessfully logs into the app once the server comes back online + * 3. Successfully sorts her recipe list in alphabetical and chronological order + * 4. Successfully filters her recipes to contain only lunch recipes + * 5. Successfully shares one of the recipes from her recipe list + */ public class EndToEndScenario2_2 { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); - Model model = new Model(); Format format = new Format(); + private AccountCSVWriter writer; // Writer that saves user credentials to a csv file + private AccountCSVReader reader; // Reader that reads user credentials from a csv file + private List userCredentials; // Helper variable that stores credentials read from the csv file + private static Account account; // Account used in the following tests + private static BaseServer server; // Mock server used in the following tests + private static Model model; // Model used in the following tests + private static RecipeBuilder b1, b2, b3, b4; // Recipe builders used to set up the recipe list + private static Recipe r1, r2, r3, r4; // Recipes that will be used in the following tests + private static List initialRecipeList; // Main recipe list used throughout the tests + + /** + * This method is a replica of our update method in RecipeListUI. It has the + * same logic, but is rewritten to not create UI elements. Additionally, it + * adds the elements to a normal List rather than a UI list. + * + * @param filter - the filter criteria selected by the user + */ + private void update(String filter) { + List temp = new ArrayList<>(); + for (Recipe recipe : initialRecipeList) { + if (filter.equals("none") || recipe.getMealTag().toLowerCase().equals(filter.toLowerCase())) { + temp.add(recipe); + } + } + initialRecipeList = temp; + } + + @BeforeAll + public static void setUp() throws IOException { + // Initialize an account for Chef Caitlyn + account = new Account("Chef", "Caitlyn"); + // Initialize a mocked server that PantryPal will "use" + server = new MockServer("localhost", AppConfig.SERVER_PORT); + // Initialize a helper model object + model = new Model(); + // Start up the server before Chef Caitlyn opens up the app + server.start(); + // Initialize an empty recipe list + initialRecipeList = new ArrayList<>(); + // Initialize recipe builders that specify only the title and date + b1 = new RecipeBuilder(account.getId(), "Recipe One"); // Alphabetically 2nd, Chronologically 2nd + b1.setDate(200); + b1.setMealTag("lunch"); + b2 = new RecipeBuilder(account.getId(), "Recipe Two"); // Alphabetically 4th, Chronologicaly 4th + b2.setDate(400); + b2.setMealTag("breakfast"); + b3 = new RecipeBuilder(account.getId(), "Recipe Three"); // Alphabetically 3rd, Chronologically 1st + b3.setDate(50); + b3.setMealTag("lunch"); + b4 = new RecipeBuilder(account.getId(), "Recipe Four"); // Alphabetically 1st, Chronologically 3rd + b4.setDate(300); + b4.setMealTag("dinner"); + // Use the recipe builders to build the recipes + r1 = b1.buildRecipe(); + r2 = b2.buildRecipe(); + r3 = b3.buildRecipe(); + r4 = b4.buildRecipe(); + // Add the recipes to the recipe list in the order r1, r2, r3, r4 + initialRecipeList.add(r1); + initialRecipeList.add(r2); + initialRecipeList.add(r3); + initialRecipeList.add(r4); + } @Test public void serverUnavailableTest() throws MalformedURLException, IOException { + server.stop(); String response = model.performAccountRequest("GET", "user", "password"); - assertTrue(response.contains("Error")); + assertTrue(response.contains("Username is not found")); response = model.performRecipeRequest("GET", "recipe", "userId"); - assertTrue(response.contains("Error")); + assertTrue(response.contains("No recipes found")); - try { - model.performWhisperRequest("GET", "mealType"); - assert (false); - } catch (ConnectException e) { - assert (true); - } + response = model.performWhisperRequest("GET", "wah"); + assertTrue(response.contains("Error")); response = model.performChatGPTRequest("GET", "mealType", "ingredients"); + String expected = ""; + assertEquals(expected, response); assertTrue(response.contains("Error")); response = model.performDallERequest("GET", "recipeTitle"); @@ -48,18 +133,142 @@ public void serverUnavailableTest() throws MalformedURLException, IOException { @Test public void loginSuccessfulTest() { + try { + server.start(); + } catch (Exception e) { + System.err.println("Server failed to start: " + e.getMessage()); + } + String successMessage = "success"; + userCredentials = new ArrayList<>(); + try { + writer = new AccountCSVWriter(new FileWriter("UserCredentialsTest.csv")); + writer.writeAccount(account.getUsername(), account.getPassword()); + writer.close(); + + reader = new AccountCSVReader(new FileReader("UserCredentialsTest.csv")); + userCredentials = reader.readUserCredentials(); + reader.close(); + + assertTrue(userCredentials.size() == 2); + assertTrue(userCredentials.get(0).equals(account.getUsername())); + assertTrue(userCredentials.get(1).equals(account.getPassword())); + + String loginResp = model.performAccountRequest("GET", account.getUsername(), account.getPassword()); + // String expected = ""; + // assertEquals(expected, loginResp); + assertTrue(loginResp.contains(successMessage), "Username is not found"); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Failed test setup"); + } + server.stop(); } + /** + * Test that Chef Caitlyn can successfully sort her recipe list in alphabetical + * and chronological order. + */ @Test public void sortRecipeListTest() { + try { + server.start(); + } catch (Exception e) { + System.err.println("Server failed to start: " + e.getMessage()); + } + + // String loginResponse = model.performAccountRequest("POST", "user", + // "password"); + // assertTrue(loginResponse.contains("success"), "Login unsuccessful"); + + // Initialize a recipe list sorter for the empty recipe list + RecipeSorter sorter = new RecipeSorter(initialRecipeList); + // Chef Caitlyn tries sorting her recipe list from oldest to newest + sorter.sortOldestToNewest(); + // Check that Chef Caitlyn's list is now in oldest to newest order + assertEquals(r3.getDate(), initialRecipeList.get(0).getDate()); + assertEquals(r1.getDate(), initialRecipeList.get(1).getDate()); + assertEquals(r4.getDate(), initialRecipeList.get(2).getDate()); + assertEquals(r2.getDate(), initialRecipeList.get(3).getDate()); + // Chef Caitlyn tries sorting her recipe list in alphabetical order + sorter.sortAToZ(); + // Check that Chef Caitlyn's list is now in alphabetical order + assertEquals(r4.getTitle(), initialRecipeList.get(0).getTitle()); + assertEquals(r1.getTitle(), initialRecipeList.get(1).getTitle()); + assertEquals(r3.getTitle(), initialRecipeList.get(2).getTitle()); + assertEquals(r2.getTitle(), initialRecipeList.get(3).getTitle()); } + /** + * Test that Chef Caitlyn can successfully filter her recipe list to only show + * lunch recipes. + */ @Test public void filterRecipeListTest() { + try { + server.start(); + } catch (Exception e) { + System.err.println("Server failed to start: " + e.getMessage()); + } + // Recipe list size should be 4 before filtering + assertTrue(initialRecipeList.size() == 4); + // Filter the recipe list to show only lunch recipes + update("lunch"); + // Filtered recipe list should only have r1 and r3 + List expected = new ArrayList<>(); + expected.add(r1); + expected.add(r3); + assertEquals(expected, initialRecipeList); + // Recipe list size should be 2 after filtering + assertTrue(initialRecipeList.size() == 2); } @Test public void shareRecipeListTest() { + try { + server.start(); + } catch (Exception e) { + System.err.println("Server failed to start: " + e.getMessage()); + } + try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { + MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); + MongoCollection accountCollection = mongoDb.getCollection(AppConfig.MONGO_USER_COLLECTION); + AccountMongoDB accountDb = new AccountMongoDB(accountCollection); + MongoCollection recipeCollection = mongoDb.getCollection(AppConfig.MONGO_RECIPE_COLLECTION); + RecipeMongoDb recipeDb = new RecipeMongoDb(recipeCollection); + String query = "localhost:8100/recipes/lol/656eb76baa1a68349d0cc61d"; + int usernameStart = query.indexOf(AppConfig.SHARE_PATH); + String username = query.substring(usernameStart + AppConfig.SHARE_PATH.length()); + String recipeID = username.substring(username.indexOf("/") + 1); + username = username.substring(0, username.indexOf("/")); + String response = ShareRecipe.getSharedRecipe(accountDb, recipeDb, username, recipeID); + String expected = ""; + /* + * title: "Coconut Mango Sticky Rice Bowl" + * mealTag: "Lunch" + */ + // Ingredients: + // 1 cup coconut milk + // 1 cup mango pieces + // 2 cups cooked sticky rice + // 1 tablespoon soy sauce + // 2 teaspoons vegetable oil + // 1 teaspoon sesame oil + // another cup of coconut milk + // Instructions: + // Once the oil is hot, add the mango pieces and cook for 2 minutes, stirring + // frequently. + // Add the soy sauce and salt to taste, and cook for another 2 minutes. + // Add the coconut milk and cooked sticky rice and cook for 3-4 minutes, + // stirring frequently. + assertTrue(response.contains("Coconut Mango Sticky Rice Bowl")); + assertTrue(response.contains("1 cup mango pieces")); + assertTrue(response.contains("Add the coconut")); + server.stop(); + } catch (Exception e) { + e.printStackTrace(); + } + // Stop the server once Chef Caitlyn is done using the app + server.stop(); } } \ No newline at end of file diff --git a/app/src/test/java/code/RecipeToImageTest.java b/app/src/test/java/code/RecipeToImageTest.java index 37bc452..63d4b44 100644 --- a/app/src/test/java/code/RecipeToImageTest.java +++ b/app/src/test/java/code/RecipeToImageTest.java @@ -14,7 +14,7 @@ import java.util.Base64; public class RecipeToImageTest { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + BaseServer server = new MockServer("localhost", AppConfig.SERVER_PORT); Model model = new Model(); /* diff --git a/app/src/test/java/code/RefreshTest.java b/app/src/test/java/code/RefreshTest.java index 3d06649..9e9168f 100644 --- a/app/src/test/java/code/RefreshTest.java +++ b/app/src/test/java/code/RefreshTest.java @@ -13,7 +13,7 @@ import java.net.URISyntaxException; public class RefreshTest { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + BaseServer server = new MockServer("localhost", AppConfig.SERVER_PORT); Model model = new Model(); @Test diff --git a/app/src/test/java/code/ServerConnectionTest.java b/app/src/test/java/code/ServerConnectionTest.java index 41c856f..b3b9c11 100644 --- a/app/src/test/java/code/ServerConnectionTest.java +++ b/app/src/test/java/code/ServerConnectionTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import code.client.Model.AppConfig; -import code.client.View.ServerConnection; +import code.client.Model.ServerConnection; import code.server.BaseServer; import code.server.mocking.MockServer; @@ -30,7 +30,7 @@ public void restoreStreams() { @Test void testServerOffline() throws IOException { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + BaseServer server = new MockServer("localhost", AppConfig.SERVER_PORT); ServerConnection connection = new ServerConnection(server); assertFalse(connection.isOnline()); assertEquals("Server is offline", outData.toString()); @@ -38,7 +38,7 @@ void testServerOffline() throws IOException { @Test void testServerOnline() throws IOException { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + BaseServer server = new MockServer("localhost", AppConfig.SERVER_PORT); ServerConnection connection = new ServerConnection(server); server.start(); assertTrue(connection.isOnline()); diff --git a/app/src/test/java/code/TextToRecipeTest.java b/app/src/test/java/code/TextToRecipeTest.java index f874bd2..b6f3267 100644 --- a/app/src/test/java/code/TextToRecipeTest.java +++ b/app/src/test/java/code/TextToRecipeTest.java @@ -15,7 +15,7 @@ import java.net.URISyntaxException; public class TextToRecipeTest { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + BaseServer server = new MockServer("localhost", AppConfig.SERVER_PORT); Model model = new Model(); Format format = new Format(); diff --git a/app/src/test/java/code/VoiceToTextTest.java b/app/src/test/java/code/VoiceToTextTest.java index 7fe4d67..1ccd59e 100644 --- a/app/src/test/java/code/VoiceToTextTest.java +++ b/app/src/test/java/code/VoiceToTextTest.java @@ -17,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class VoiceToTextTest { - BaseServer server = new MockServer(AppConfig.SERVER_HOST, AppConfig.SERVER_PORT); + BaseServer server = new MockServer("localhost", AppConfig.SERVER_PORT); Model model = new Model(); /* From ebf5a2d39431ec5d57ead4fa70db9b4aa2f04f33 Mon Sep 17 00:00:00 2001 From: AllKeng Date: Tue, 5 Dec 2023 23:12:11 -0800 Subject: [PATCH 30/31] Adjusted tests to actually run in localhost. Co-authored-by: kjanderson1 Co-authored-by: ChristophianSulaiman Co-authored-by: dashluu Co-authored-by: Timoji Co-authored-by: Samantha Prestrelski --- .../java/code/client/Model/AppConfig.java | 2 +- .../main/java/code/client/Model/Model.java | 5 ++- .../mocking/MockChatGPTRequestHandler.java | 2 +- .../java/code/server/mocking/MockServer.java | 2 +- .../test/java/code/EndToEndScenario2_1.java | 36 +++++++++++++++++-- .../test/java/code/EndToEndScenario2_2.java | 26 +++++++++----- 6 files changed, 59 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/code/client/Model/AppConfig.java b/app/src/main/java/code/client/Model/AppConfig.java index f5eb263..811632e 100644 --- a/app/src/main/java/code/client/Model/AppConfig.java +++ b/app/src/main/java/code/client/Model/AppConfig.java @@ -22,7 +22,7 @@ public class AppConfig { public static final String MONGO_RECIPE_COLLECTION = "recipes"; public static final String MONGO_USER_COLLECTION = "users"; // server - public static final String SERVER_HOST = "192.168.1.123"; + public static final String SERVER_HOST = "localhost"; public static final int SERVER_PORT = 8100; public static final String SERVER_URL = "http://" + SERVER_HOST + ":" + SERVER_PORT; public static final String RECIPE_PATH = "/recipe"; diff --git a/app/src/main/java/code/client/Model/Model.java b/app/src/main/java/code/client/Model/Model.java index c3c0bb5..7b21891 100644 --- a/app/src/main/java/code/client/Model/Model.java +++ b/app/src/main/java/code/client/Model/Model.java @@ -75,7 +75,7 @@ public String performRecipeRequest(String method, String recipe, String userId) while ((line = in.readLine()) != null) { tempResponse += line + "\n"; } - if( !(tempResponse.toLowerCase().contains("error")) ) { + if (!(tempResponse.toLowerCase().contains("error"))) { response = tempResponse; } in.close(); @@ -170,6 +170,9 @@ public String performWhisperRequest(String method, String type) throws Malformed } in.close(); + } catch (Exception e) { + e.printStackTrace(); + response = "Error: " + e; } System.out.println("Whisper response: " + response); diff --git a/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java b/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java index cc2823d..33e6b31 100644 --- a/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java +++ b/app/src/main/java/code/server/mocking/MockChatGPTRequestHandler.java @@ -39,7 +39,7 @@ public void handle(HttpExchange httpExchange) throws IOException { String mealType = typeIngredients[0]; String ingredients = typeIngredients[1]; - if (mealType.equals("Breakfast")) { + if (mealType.toLowerCase().equals("breakfast")) { response = sampleRecipe; } } catch (IndexOutOfBoundsException e) { diff --git a/app/src/main/java/code/server/mocking/MockServer.java b/app/src/main/java/code/server/mocking/MockServer.java index e9f815a..d29c6df 100644 --- a/app/src/main/java/code/server/mocking/MockServer.java +++ b/app/src/main/java/code/server/mocking/MockServer.java @@ -45,7 +45,7 @@ public void start() throws IOException { // create a map to store data // create a server httpServer = HttpServer.create( - new InetSocketAddress("0.0.0.0", port), + new InetSocketAddress(hostName, port), 0); // create the context to map urls httpServer.createContext(AppConfig.RECIPE_PATH, new RecipeRequestHandler(recipeDb)); diff --git a/app/src/test/java/code/EndToEndScenario2_1.java b/app/src/test/java/code/EndToEndScenario2_1.java index b1b134a..f704c08 100644 --- a/app/src/test/java/code/EndToEndScenario2_1.java +++ b/app/src/test/java/code/EndToEndScenario2_1.java @@ -50,7 +50,6 @@ public static void setUp() throws IOException { // Initialize a helper model object model = new Model(); // Start up the server before Chef Caitlyn opens up the app - server.start(); } /** @@ -58,6 +57,12 @@ public static void setUp() throws IOException { */ @Test public void createAccountTest() { + try { + server.start(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); MongoCollection accountCollection = mongoDb.getCollection(AppConfig.MONGO_USER_COLLECTION); @@ -68,6 +73,7 @@ public void createAccountTest() { } catch (Exception e) { e.printStackTrace(); } + server.stop(); } /** @@ -76,6 +82,12 @@ public void createAccountTest() { */ @Test public void automaticLoginTest() throws IOException { + try { + server.start(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } // Save the username and password to a file called "UserCredentialsTest.csv" AccountCSVWriter writer = new AccountCSVWriter(new FileWriter("UserCredentialsTest.csv")); writer.writeAccount(account.getUsername(), account.getPassword()); @@ -89,6 +101,7 @@ public void automaticLoginTest() throws IOException { String expectedPassword = "Caitlyn"; assertEquals(expectedUsername, userCredentials.get(0)); assertEquals(expectedPassword, userCredentials.get(1)); + server.stop(); } /** @@ -96,6 +109,12 @@ public void automaticLoginTest() throws IOException { */ @Test public void createRecipeTest() { + try { + server.start(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } // Perform a mock ChatGPT request for Chef Caitlyn's fried chicken recipe String mealType = "breakfast"; String ingredients = "chicken, eggs"; @@ -113,6 +132,7 @@ public void createRecipeTest() { """; // Check that the recipe was created successfully from the ChatGPT response assertEquals(expectedResponse, initialResponse); + server.stop(); } /** @@ -121,7 +141,12 @@ public void createRecipeTest() { */ @Test public void refreshRecipeTest() { - + try { + server.start(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } /* START OF COPIED TEST CONTENT */ String mealType = "breakfast"; @@ -146,6 +171,7 @@ public void refreshRecipeTest() { String refreshResponse = model.performChatGPTRequest("PUT", mealType, ingredients); // Check that the recipe body is no longer the same assertNotEquals(initialResponse, refreshResponse); + server.stop(); } /** @@ -153,6 +179,12 @@ public void refreshRecipeTest() { */ @Test public void saveRecipeTest() { + try { + server.start(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } // Build a recipe based on the mocked refreshed recipe from the previous test RecipeBuilder builder = new RecipeBuilder(account.getId(), "Fried Chicken and Egg Fried Rice"); builder.setMealTag("breakfast"); diff --git a/app/src/test/java/code/EndToEndScenario2_2.java b/app/src/test/java/code/EndToEndScenario2_2.java index 0ef90e2..fe2c5e6 100644 --- a/app/src/test/java/code/EndToEndScenario2_2.java +++ b/app/src/test/java/code/EndToEndScenario2_2.java @@ -1,6 +1,8 @@ package code; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.FileReader; @@ -107,23 +109,22 @@ public static void setUp() throws IOException { initialRecipeList.add(r2); initialRecipeList.add(r3); initialRecipeList.add(r4); + server.stop(); } @Test public void serverUnavailableTest() throws MalformedURLException, IOException { server.stop(); String response = model.performAccountRequest("GET", "user", "password"); - assertTrue(response.contains("Username is not found")); + assertTrue(response.contains("Error")); response = model.performRecipeRequest("GET", "recipe", "userId"); - assertTrue(response.contains("No recipes found")); + assertTrue(response.contains("Error")); response = model.performWhisperRequest("GET", "wah"); assertTrue(response.contains("Error")); response = model.performChatGPTRequest("GET", "mealType", "ingredients"); - String expected = ""; - assertEquals(expected, response); assertTrue(response.contains("Error")); response = model.performDallERequest("GET", "recipeTitle"); @@ -140,6 +141,7 @@ public void loginSuccessfulTest() { } String successMessage = "success"; userCredentials = new ArrayList<>(); + try { writer = new AccountCSVWriter(new FileWriter("UserCredentialsTest.csv")); writer.writeAccount(account.getUsername(), account.getPassword()); @@ -153,10 +155,18 @@ public void loginSuccessfulTest() { assertTrue(userCredentials.get(0).equals(account.getUsername())); assertTrue(userCredentials.get(1).equals(account.getPassword())); - String loginResp = model.performAccountRequest("GET", account.getUsername(), account.getPassword()); - // String expected = ""; - // assertEquals(expected, loginResp); - assertTrue(loginResp.contains(successMessage), "Username is not found"); + try (MongoClient mongoClient = MongoClients.create(AppConfig.MONGODB_CONN)) { + MongoDatabase mongoDb = mongoClient.getDatabase(AppConfig.MONGO_DB); + MongoCollection accountCollection = mongoDb.getCollection(AppConfig.MONGO_USER_COLLECTION); + AccountMongoDB accountDb = new AccountMongoDB(accountCollection); + accountDb.add(account); + String loginResp = model.performAccountRequest("GET", account.getUsername(), account.getPassword()); + assertNotEquals("Username is not found", loginResp); + assertNotEquals("Incorrect password", loginResp); + accountDb.removeByUsername("Chef"); + } catch (Exception e) { + e.printStackTrace(); + } } catch (IOException e) { e.printStackTrace(); System.err.println("Failed test setup"); From 5843feba0475d11d3247864319ea942b7cfc829b Mon Sep 17 00:00:00 2001 From: AllKeng Date: Tue, 5 Dec 2023 23:15:18 -0800 Subject: [PATCH 31/31] Fixed unclosed server starts. --- app/src/test/java/code/EndToEndScenario2_2.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/test/java/code/EndToEndScenario2_2.java b/app/src/test/java/code/EndToEndScenario2_2.java index fe2c5e6..c0d6a61 100644 --- a/app/src/test/java/code/EndToEndScenario2_2.java +++ b/app/src/test/java/code/EndToEndScenario2_2.java @@ -206,6 +206,7 @@ public void sortRecipeListTest() { assertEquals(r1.getTitle(), initialRecipeList.get(1).getTitle()); assertEquals(r3.getTitle(), initialRecipeList.get(2).getTitle()); assertEquals(r2.getTitle(), initialRecipeList.get(3).getTitle()); + server.stop(); } /** @@ -230,6 +231,7 @@ public void filterRecipeListTest() { assertEquals(expected, initialRecipeList); // Recipe list size should be 2 after filtering assertTrue(initialRecipeList.size() == 2); + server.stop(); } @Test