diff --git a/LICENSE b/LICENSE index 0e259d42c..cffe7a70f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,121 +1,21 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. +MIT License + +Copyright (c) 2024 Jongyoon Baek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5e1d8b77c..1f5205b11 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,254 @@ -# Note Application +# Recipe Wiz -This is a minimal example demonstrating usage of the -password-protected user part of the API used in lab 5. +## Project Overview -You can find more information about the API endpoints in -[the documentation](https://www.postman.com/cloudy-astronaut-813156/csc207-grade-apis-demo/documentation/fg3zkjm/5-password-protected-user). +Recipe Wiz is an innovative meal planning and recipe management application designed to simplify your cooking experience. It empowers users to discover recipes based on available ingredients, dietary preferences, or specific meal types. With features like nutritional analysis, a customizable meal planner, shopping list automation, and adjustable serving sizes, Recipe Wiz aims to make meal preparation both efficient and health-conscious. -If your team is considering an application for which it would be convenient to -store data in something like a database, you may find that the API calls demonstrated -here will be useful in your project, as this will allow you to store -an arbitrary JSON object associated with a username and password. +## Contents -In this application, a single note has a name (the "username" in terms of the API) and the note -can be read by anyone who knows the name — but only edited by someone who -knows the password for it. +1. [Authors and contributors](#Authors-and-contributors) +2. [Key Features](#Key-Features) +2. [Installation Instructions](#Installation-Instructions) +3. [Usage Guide](#usage-guide) +4. [License](#License) +5. [Support and Feedback](#support-and-feedback) +6. [How to Contribute](#how-to-contribute) +7. [API USAGE](#API-USAGE) -You can see the documentation in the various files for more information. +## Authors and contributors +- Haris Anjum + - (@hanjum66) +- Jongyoon Baek + - (@KaiBaek) +- Xineng Na + - (@Alan-Na) +- Yujie Zeng + - (@Jerry-Zeng-UofT) -## Testing +## Key Features -The repo also includes an example of a use case interactor test, as well as -an example of an end-to-end test which automates button clicks and inspects -the contents of the actual views. This is something we discussed in the lectures -about testing in CA but had not provided a code example of before. Note, one -could also inspect the contents of the ViewModel objects instead when testing -CA to make a similar test which would be less dependent on the details of the -specific UI implementation. +- **Smart Recipe Search** + - Find recipes using ingredients you have on hand. + - Filter results by meal types and dietary restrictions. + - Leverage the Edamam Recipe API for a vast selection. -## Project Starter Code +- **Detailed Nutritional Information** + - Access comprehensive nutritional data for each recipe. + - Includes calories, macronutrients, and other essential nutrients. + +- **Personalized Meal Planner** + - Schedule meals in a weekly calendar format. + - Add, modify, or remove recipes from your plan easily. + - Save your meal plans for future reference. + +- **Adjustable Serving Sizes** + - Modify recipes to fit the number of servings you need. + - Automatic recalculation of ingredient amounts. + +- **Dietary Preferences and Restrictions** + - Apply filters for gluten-free, vegetarian, vegan, low-carb, and more. + - Ensures all recipes meet your dietary needs. + +## Installation Instructions + +Before installing Recipe Wiz, ensure your system meets the following requirements: +1. **Java Development Kit (JDK) version 22 or higher** + - Download the latest JDK from [Oracle](https://www.oracle.com/java/technologies/downloads/) + +2. **Language Level** + - Set the project language level to **16** in your IDE settings. + +3. **Integrated Development Environment (IDE)** + - Recommended: [IntelliJ IDEA](https://www.jetbrains.com/idea/download/). + +### Installation Steps + +1. **Clone main branch of the Repository** + + ```bash + git clone https://github.com/Jerry-Zeng-UofT/Group-project-209.git + +2. **Open the Project in IntelliJ IDEA** + + - Launch IntelliJ IDEA. Click on File > Open. Navigate to the cloned recipe-wiz directory and select it. +3. **Configure the JDK and Language Level** + + - Go to File > Project Structure. Under Project Settings > Project, set the Project SDK to your installed JDK 22 or higher. In the Project Structure dialog, under Project Settings > Modules, select your module. In the Sources tab, set the Language Level to 16 - Records, patterns, local enums and interfaces. +4. **Import Project Dependencies** + + - Since the project uses Maven, right-click on the pom.xml and select Maven > Reload Project + +5. **Run the Application** + + - In the Project Explorer, locate MainRecipeApplication.java and select Run ‘MainRecipeApplication.main()’. The application should start, and you can begin using Recipe Wiz. + +### Common Installation Issues + +1. **Dependency Resolution Problems** + + - Try refreshing the project dependencies: Right-click on the project and select Maven > Reload Project. + + +2. **Incorrect JDK Version** + + - In your IDE, ensure that the Project SDK is set to JDK 22 or higher. Check that individual modules are also set to use the correct SDK. + + +3. **Language Level Issues** + + - If you encounter errors related to language features, make sure the Project Language Level is set to 16. Go to File > Project Structure > Project, and set the Project Language Level accordingly. Also, check the Module Language Level under Project Settings > Modules. + + +## Usage Guide + +The moment you run the program, you should see the front page view. If you click “Recipe Search” in that window, you'll be taken to a recipe search window, and if you click Meal planner, you'll see a meal planning calendar. + +![frontpageview.png](src/main/java/resources/frontpageview.png) + +### How to use Recipe Search + +1. You can add an ingredient to the ingredients panel by writing it in the ingredient field and hitting the add ingredient button. Conversely, you can remove an ingredient from the ingredients pane by selecting it in the ingredients panel and pressing the remove ingredient button. + +![readme1.png](src/main/java/resources/readme1.png) + +2. You can filter your recipe search by pressing the add restriction button. This button will open a filter frame window where you can select Diet Label, Health Label, Cuisine Type, etc. in the pop-up window. You can remove a restriction from the restrictions panel by selecting it in the same way as remove ingredient and clicking the remove restriction button. + +![readme2.png](src/main/java/resources/readme2.png) + +3. The Search button allows you to search for recipes based on the given ingredients and restrictions. + +![readme3.png](src/main/java/resources/readme3.png) + +4. If you enter a number in the servings field and then update, the recipe results in the results panel will change to match the given servings value. + +![readme4.png](src/main/java/resources/readme4.png) + +5. If the analyze nutrition button is pressed after selecting a specific recipe, the user can view the nutritional information for that specific recipe. + +![readme5.png](src/main/java/resources/readme5.png) + +6. When you press the Save Recipe button, a specific recipe is saved to your meal planning calendar. + + + +### How to use Meal Planner + +1. Recipes saved from the recipe search bar will be available in the meal planning calendar, allowing users to customize their meal planning calendar. + +2. Users can select different meal types and add recipes to the meal planning calendar to be eaten on the desired date. + +3. Recipes saved in the meal planning calendar will remain as separate json files even after the program is terminated, and when the user runs the program again, the previously saved meal planning calendar will be loaded. + +![readme6.png](src/main/java/resources/readme6.png) + +## License +- This project is licensed under the **MIT License**, which means you are free to: + - Use the code for personal or commercial purposes. + - Modify and distribute the code. + - Include the code in proprietary software. + + +- However, you **must**: + - Include the original copyright notice and this permission notice in any copies of the software. + - Not hold the original authors liable for any damages arising from the use of the software. + + +- You can view the full text of the MIT License in the [LICENSE](./LICENSE) file included in this repository. +## Support and Feedback + +We value your input and encourage you to provide feedback to help us improve Recipe Wiz. Here’s how you can share your thoughts and suggestions: + +1. **GitHub Issues** + + - Submit feedback, report bugs, or suggest new features by creating an issue in the GitHub [Issues](https://github.com/Jerry-Zeng-UofT/Group-project-209/issues) section. + + +2. **Discussion Board** + + - Join discussions with the community and developers in our GitHub [Discussions](https://github.com/Jerry-Zeng-UofT/Group-project-209/discussions) + +### Feedback Guidelines + +**Valid Feedback Includes**: + +1. Clear descriptions of issues or suggestions. +2. Steps to reproduce bugs, if applicable. +3. Relevant screenshots, logs, or examples to support your points. + +**What to Expect** + +1. We aim to review and respond to feedback within 1 week. +2. Once verified, we will prioritize fixes based on severity. +3. Contributors who provide actionable feedback that results in improvements may receive acknowledgment in project updates. + + +## How to Contribute + +We welcome contributions from the community to improve Recipe Wiz! + +### Contribution Steps + +1. **Fork the Repository** + + - Click the Fork button on the top-right corner of the repository page. + +2. **Clone Your Fork** + + ```bash + git clone https://github.com/your-username/Group-project-209.git +3. **Create a New Branch** + + - Create a branch where you make fixes. + +4. **Implement your changes** + + - Fix bugs, create new features, or contribute code in your created branch from the previous step. + +5. **Commit Your Changes** + + - After any changes, commit your changes with meaningful commit message. + +6. **Push Your Changes** + +7. **Submit a Pull Request** + + - Go to the original repository and click on New Pull Request. + - Provide a clear and detailed description of your changes. + +## API Usage + +In Recipe Wiz, we leverage the powerful **Edamam API** to enhance the user experience by providing comprehensive recipe search and nutritional analysis capabilities. Below is an overview of the APIs used and their functionality: + +### 1. Recipe Search API + +The **Recipe Search API** from Edamam is utilized to enable users to: + +- Search for recipes based on ingredients, dietary restrictions, and meal types. +- Retrieve a rich dataset for each recipe, including: + - Recipe title, image, and preparation steps. + - Ingredients list with quantities. + - Dietary labels (e.g., gluten-free, vegetarian). + - Health labels (e.g., low-carb, keto-friendly). + +This API ensures that Recipe Wiz provides tailored and diverse recipe suggestions for users with varying preferences and dietary needs. + +### 2. Nutritional Analysis API + +The **Nutritional Analysis API** from Edamam powers the nutritional information feature in Recipe Wiz. This API is used to: + +- Analyze recipes to calculate detailed nutritional data, including: + - Calories. + - Macronutrients (protein, fat, carbohydrates). + - Micronutrients (fiber, sugar, vitamins). +- Display easy-to-understand nutritional breakdowns for each recipe, helping users make informed dietary choices. + +### API Documentation + +For more details about the APIs used, refer to the official Edamam API documentation: + +- [Recipe Search API Documentation](https://developer.edamam.com/edamam-recipe-api) +- [Nutritional Analysis API Documentation](https://developer.edamam.com/edamam-nutrition-api) + + -Your team may choose to use this repo as starter code for your project. You could -also use the lab 5 code — or start from an empty repo if your team prefers. -If you choose to use one of the repositories we have provided, you can either make -a fork of it or copy the subset of code you want into a completely new repository. diff --git a/pom.xml b/pom.xml index 527f61e36..fff4035f8 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,12 @@ 20240303 + + com.google.code.gson + gson + 2.10.1 + + com.squareup.okhttp3 okhttp @@ -27,9 +33,22 @@ 4.13.1 test + test + - + + + + org.apache.maven.plugins + maven-compiler-plugin + + 16 + 16 + + + + diff --git a/src/main/java/app/MainNoteApplication.java b/src/main/java/app/MainNoteApplication.java deleted file mode 100644 index c37860156..000000000 --- a/src/main/java/app/MainNoteApplication.java +++ /dev/null @@ -1,56 +0,0 @@ -package app; - -import data_access.DBNoteDataAccessObject; -import use_case.note.NoteDataAccessInterface; - -/** - * An application where we can view and add to a note stored by a user. - *

- * This is a minimal example of using the password-protected user API from lab 5, - * but demonstrating the endpoint allowing you to store an arbitrary JSON object. - * This functionality could be used in any project where your team wants to persist - * data which is then accessible across devices.

- *

The code is intentionally somewhat incomplete to leave work to be done if your - * team were to choose to work on a project which would require similar functionality. - * For example, we have intentionally not created a full "Note" entity here, but - * rather just represented a note as a string. - *

- * The ViewManager code has also been removed, since this minimal program only requires a single - * view. Your team may wish to bring back the ViewManager or make your own implementation of supporting - * switching between views depending on your project. - */ -public class MainNoteApplication { - - /** - * The main entry point of the application. - *

- * The program will show you the note currently saved in the system. - * You are able to edit it and then save it to the system. You can refresh - * to update the note to reflect what was saved most recently. This - * uses the API from lab, so there is one database storing the note, - * which means that if anyone updates the note, that is what you will - * see when you refresh. - *

- * You can generalize the code to allow you to - * specify which "user" to save the note for, which will allow your team - * to store information specific to your team which is password-protected. - * The username and password used in this application are currently for - * user jonathan_calver2, but you can change that. As you did in lab 3, - * you will likely want to store password information locally rather than - * in your repo. Or you can require the user to enter their credentials - * in your application; it just depends on what your program's main - * functionality. - *

- * @param args commandline arguments are ignored - */ - public static void main(String[] args) { - - // create the data access and inject it into our builder! - final NoteDataAccessInterface noteDataAccess = new DBNoteDataAccessObject(); - - final NoteAppBuilder builder = new NoteAppBuilder(); - builder.addNoteDAO(noteDataAccess) - .addNoteView() - .addNoteUseCase().build().setVisible(true); - } -} diff --git a/src/main/java/app/MainRecipeApplication.java b/src/main/java/app/MainRecipeApplication.java new file mode 100644 index 000000000..eb462f79a --- /dev/null +++ b/src/main/java/app/MainRecipeApplication.java @@ -0,0 +1,42 @@ +package app; + +import data_access.RecipeSearchDataAccessObject; +import data_access.SearchWithRestrictionDataAccessObject; + +/** + * An application where users can search for recipes based on ingredients. + *

+ * This application allows users to input ingredients and search for matching recipes + * using the Edamam Recipe Search API. Users can view recipe details including + * ingredients and instructions. + *

+ */ +public class MainRecipeApplication { + /** + * Main method for the Recipe Application. + * @param args The command line arguments + */ + public static void main(String[] args) { + // Create the data access object for recipe search + final RecipeSearchDataAccessObject recipeSearchDataAccessObject = new RecipeSearchDataAccessObject(); + final SearchWithRestrictionDataAccessObject searchWithRestrictionDataAccessObject = new SearchWithRestrictionDataAccessObject(); + + // Create and configure the application using the builder + final RecipeAppBuilder builder = new RecipeAppBuilder(); + + // Build the application with correct order + builder.addRecipeSearchAPI(recipeSearchDataAccessObject, searchWithRestrictionDataAccessObject) + .addMealPlanningView() + .addMealPlanningUseCase() + .addNutritionAnalysisView() + .addRecipeSearchView() + .addRecipeSearchUseCase() + .addRestrictionSearchUseCase() + .addNutritionAnalysisUseCase() + .addServingAdjustUseCase() + .addFrontpageView(); + + // Build and show the frame + builder.build(); + } +} diff --git a/src/main/java/app/NoteAppBuilder.java b/src/main/java/app/NoteAppBuilder.java deleted file mode 100644 index a68cb9ad6..000000000 --- a/src/main/java/app/NoteAppBuilder.java +++ /dev/null @@ -1,83 +0,0 @@ -package app; - -import javax.swing.JFrame; -import javax.swing.WindowConstants; - -import interface_adapter.note.NoteController; -import interface_adapter.note.NotePresenter; -import interface_adapter.note.NoteViewModel; -import use_case.note.NoteDataAccessInterface; -import use_case.note.NoteInteractor; -import use_case.note.NoteOutputBoundary; -import view.NoteView; - -/** - * Builder for the Note Application. - */ -public class NoteAppBuilder { - public static final int HEIGHT = 300; - public static final int WIDTH = 400; - private NoteDataAccessInterface noteDAO; - private NoteViewModel noteViewModel = new NoteViewModel(); - private NoteView noteView; - private NoteInteractor noteInteractor; - - /** - * Sets the NoteDAO to be used in this application. - * @param noteDataAccess the DAO to use - * @return this builder - */ - public NoteAppBuilder addNoteDAO(NoteDataAccessInterface noteDataAccess) { - noteDAO = noteDataAccess; - return this; - } - - /** - * Creates the objects for the Note Use Case and connects the NoteView to its - * controller. - *

This method must be called after addNoteView!

- * @return this builder - * @throws RuntimeException if this method is called before addNoteView - */ - public NoteAppBuilder addNoteUseCase() { - final NoteOutputBoundary noteOutputBoundary = new NotePresenter(noteViewModel); - noteInteractor = new NoteInteractor( - noteDAO, noteOutputBoundary); - - final NoteController controller = new NoteController(noteInteractor); - if (noteView == null) { - throw new RuntimeException("addNoteView must be called before addNoteUseCase"); - } - noteView.setNoteController(controller); - return this; - } - - /** - * Creates the NoteView and underlying NoteViewModel. - * @return this builder - */ - public NoteAppBuilder addNoteView() { - noteViewModel = new NoteViewModel(); - noteView = new NoteView(noteViewModel); - return this; - } - - /** - * Builds the application. - * @return the JFrame for the application - */ - public JFrame build() { - final JFrame frame = new JFrame(); - frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - frame.setTitle("Note Application"); - frame.setSize(WIDTH, HEIGHT); - - frame.add(noteView); - - // refresh so that the note will be visible when we start the program - noteInteractor.executeRefresh(); - - return frame; - - } -} diff --git a/src/main/java/app/RecipeAppBuilder.java b/src/main/java/app/RecipeAppBuilder.java new file mode 100644 index 000000000..9af1effd0 --- /dev/null +++ b/src/main/java/app/RecipeAppBuilder.java @@ -0,0 +1,270 @@ +package app; + +import javax.swing.*; +import data_access.*; + +import interface_adapter.nutrition_analysis.NutritionAnalysisController; +import interface_adapter.nutrition_analysis.NutritionAnalysisPresenter; +import interface_adapter.nutrition_analysis.NutritionAnalysisViewModel; +import interface_adapter.recipe_search.*; +import interface_adapter.meal_planning.*; +import interface_adapter.search_with_restriction.RestrictionController; +import interface_adapter.search_with_restriction.RestrictionPresenter; +import interface_adapter.search_with_restriction.RestrictionViewModel; +import interface_adapter.serving_adjust.ServingAdjustController; +import interface_adapter.serving_adjust.ServingAdjustPresenter; +import interface_adapter.serving_adjust.ServingAdjustViewModel; +import use_case.nutrition_analysis.NutritionAnalysis; +import use_case.nutrition_analysis.NutritionAnalysisInteractor; +import use_case.recipe_search.*; +import use_case.meal_planning.*; +import use_case.search_with_restriction.RecipeSearchWithRestrictionInteractor; +import use_case.search_with_restriction.SearchWithRestrictionInputBoundary; +import use_case.serving_adjust.ServingAdjustInputBoundary; +import use_case.serving_adjust.ServingAdjustInteractor; +import use_case.serving_adjust.ServingAdjustOutputBoundary; +import view.*; + +/** + * Builder class for the RecipeApp. + */ +public class RecipeAppBuilder { + public static final int HEIGHT = 600; + public static final int WIDTH = 800; + + private RecipeSearchDataAccessObject recipeSearchDataAccessObject; + private SearchWithRestrictionDataAccessObject searchWithRestrictionDataAccessObject; + private SavedRecipesDataAccessInterface savedRecipesDataAccessInterface; + private MealPlanningDataAccessInterface mealPlanningDataAccessInterface; + private NutritionAnalysisDataAccessObject nutritionAnalysisDataAccess; + + private RecipeSearchViewModel recipeSearchViewModel; + private RestrictionViewModel restrictionViewModel; + private MealPlanningViewModel mealPlanningViewModel; + private NutritionAnalysisViewModel nutritionAnalysisViewModel; + + private FrontPageView frontPageView; + private RecipeSearchView recipeSearchView; + private MealPlanningView mealPlanningView; + private NutritionAnalysisView nutritionAnalysisView; + + private RecipeSearchInputBoundary recipeSearchInputBoundaryUseCase; + private SearchWithRestrictionInputBoundary searchWithRestrictionInputBoundaryUseCase; + private MealPlanningInputBoundary mealPlanningInputBoundaryUseCase; + private NutritionAnalysis nutritionAnalysisUseCase; + + /** + * Add the recipe search API. + * @param recipeSearchDataAccessObject The recipe search API + * @param searchWithRestrictionDataAccessObject The restriction search API + * @return The builder instance + */ + public RecipeAppBuilder addRecipeSearchAPI(RecipeSearchDataAccessObject recipeSearchDataAccessObject, SearchWithRestrictionDataAccessObject searchWithRestrictionDataAccessObject) { + this.recipeSearchDataAccessObject = recipeSearchDataAccessObject; + this.searchWithRestrictionDataAccessObject = searchWithRestrictionDataAccessObject; + this.savedRecipesDataAccessInterface = new SavedRecipesDataAccessObject(); + this.mealPlanningDataAccessInterface = new MealPlanningDataAccessObject(this.savedRecipesDataAccessInterface); + this.nutritionAnalysisDataAccess = new NutritionAnalysisDataAccessObject(); + return this; + } + + /** + * Add the meal planning view. + * @return The builder instance + */ + public RecipeAppBuilder addMealPlanningView() { + mealPlanningViewModel = new MealPlanningViewModel(); + mealPlanningView = new MealPlanningView(mealPlanningViewModel); + return this; + } + + /** + * Add the meal planning view. + * @return The builder instance + */ + public RecipeAppBuilder addNutritionAnalysisView() { + nutritionAnalysisViewModel = new NutritionAnalysisViewModel(); + nutritionAnalysisView = new NutritionAnalysisView(nutritionAnalysisViewModel); + return this; + } + + /** + * Add the recipe search view. + * @return The builder instance + */ + public RecipeAppBuilder addRecipeSearchView() { + // Make sure mealPlanningView is created before this + if (mealPlanningView == null) { + throw new RuntimeException("addMealPlanningView must be called before addRecipeSearchView"); + } + if (nutritionAnalysisView == null) { + throw new RuntimeException("addNutritionAnalysisView must be called before addRecipeSearchView"); + } + + recipeSearchViewModel = new RecipeSearchViewModel(); + restrictionViewModel = new RestrictionViewModel(); + + RecipeSearchPresenter presenter = new RecipeSearchPresenter(recipeSearchViewModel); + recipeSearchInputBoundaryUseCase = new RecipeSearchInteractor(recipeSearchDataAccessObject, savedRecipesDataAccessInterface, presenter); + RecipeSearchController controller = new RecipeSearchController(recipeSearchInputBoundaryUseCase); + + RestrictionPresenter restrictionPresenter = new RestrictionPresenter(restrictionViewModel); + searchWithRestrictionInputBoundaryUseCase = new RecipeSearchWithRestrictionInteractor(searchWithRestrictionDataAccessObject, restrictionPresenter); + RestrictionController restrictionController = new RestrictionController(searchWithRestrictionInputBoundaryUseCase); + + ServingAdjustViewModel servingAdjustViewModel = new ServingAdjustViewModel(); + ServingAdjustPresenter servingAdjustPresenter = new ServingAdjustPresenter(servingAdjustViewModel); + ServingAdjustInputBoundary servingAdjustUseCase = new ServingAdjustInteractor(servingAdjustPresenter); + ServingAdjustController servingAdjustController = new ServingAdjustController(servingAdjustUseCase); + + recipeSearchView = new RecipeSearchView(recipeSearchViewModel, restrictionViewModel, controller, + restrictionController, servingAdjustController); + recipeSearchView.setMealPlanningView(mealPlanningView); + recipeSearchView.setNutritionAnalysisView(nutritionAnalysisView); + + return this; + } + + /** + * Add the front page view. + */ + public void addFrontpageView() { + frontPageView = new FrontPageView(recipeSearchView, mealPlanningView); + } + + /** + * Add the meal planning use case. + * @return The builder instance + */ + public RecipeAppBuilder addMealPlanningUseCase() { + if (mealPlanningView == null) { + throw new RuntimeException("addMealPlanningView must be called before addMealPlanningUseCase"); + } + + MealPlanningPresenter presenter = new MealPlanningPresenter(mealPlanningViewModel); + mealPlanningInputBoundaryUseCase = new MealPlanningInteractor( + mealPlanningDataAccessInterface, + savedRecipesDataAccessInterface, + presenter + ); + MealPlanningController controller = new MealPlanningController(mealPlanningInputBoundaryUseCase); + mealPlanningView.setController(controller); + + return this; + } + + /** + * Add the Nutrition Analysis Use Case. + * @return The builder instance + */ + public RecipeAppBuilder addNutritionAnalysisUseCase() { + if (nutritionAnalysisView == null) { + throw new RuntimeException("addNutritionAnalysisView must be called before addNutritionAnalysisUseCase"); + } + + NutritionAnalysisPresenter presenter = new NutritionAnalysisPresenter(nutritionAnalysisViewModel); + nutritionAnalysisUseCase = new NutritionAnalysisInteractor( + nutritionAnalysisDataAccess, + presenter + ); + NutritionAnalysisController controller = new NutritionAnalysisController(nutritionAnalysisUseCase); + recipeSearchView.setNutritionAnalysisController(controller); + + return this; + } + + /** + * Add the recipe search use case. + * @return The builder instance + */ + public RecipeAppBuilder addRecipeSearchUseCase() { + if (recipeSearchView == null) { + throw new RuntimeException("addRecipeSearchView must be called before addRecipeSearchUseCase"); + } + + RecipeSearchPresenter presenter = new RecipeSearchPresenter(recipeSearchViewModel); + recipeSearchInputBoundaryUseCase = new RecipeSearchInteractor( + recipeSearchDataAccessObject, + savedRecipesDataAccessInterface, + presenter + ); + RecipeSearchController controller = new RecipeSearchController(recipeSearchInputBoundaryUseCase); + recipeSearchView.setRecipeSearchController(controller); + + return this; + } + + /** + * Add the recipe search with restriction use case. + * @return The builder instance + */ + public RecipeAppBuilder addRestrictionSearchUseCase() { + if (recipeSearchView == null) { + throw new RuntimeException("addRecipeSearchView must be called before addRecipeSearchUseCase"); + } + + RestrictionPresenter restrictionPresenter = new RestrictionPresenter(restrictionViewModel); + searchWithRestrictionInputBoundaryUseCase = new RecipeSearchWithRestrictionInteractor( + searchWithRestrictionDataAccessObject, + restrictionPresenter + ); + RestrictionController controller = new RestrictionController(searchWithRestrictionInputBoundaryUseCase); + recipeSearchView.setRestrictionController(controller); + + return this; + } + + /** + * Add the serving adjust use case. + * @return The builder instance + */ + public RecipeAppBuilder addServingAdjustUseCase() { + ServingAdjustViewModel servingAdjustViewModel = new ServingAdjustViewModel(); + + ServingAdjustOutputBoundary presenter = new ServingAdjustPresenter(servingAdjustViewModel); + ServingAdjustInputBoundary servingAdjustUseCase = new ServingAdjustInteractor(presenter); + + ServingAdjustController controller = new ServingAdjustController(servingAdjustUseCase); + + if (recipeSearchView == null) { + throw new RuntimeException("addRecipeSearchView must be called before addServingAdjustUseCase"); + } + + recipeSearchView.setServingAdjustController(controller); + recipeSearchView.setServingAdjustViewModel(servingAdjustViewModel); + + return this; + } + + /** + * Get the saved recipes data access. + * @return The saved recipes data access + */ + public SavedRecipesDataAccessInterface getSavedRecipesDataAccess() { + return savedRecipesDataAccessInterface; + } + + /** + * Get the Nutrition Analysis data access. + * @return The Nutrition Analysis data access + */ + public NutritionAnalysisDataAccessObject getNutritionAnalysisDataAccess() { + return nutritionAnalysisDataAccess; + } + + /** + * Get the meal planning data access. + * @return The meal planning data access + */ + public MealPlanningDataAccessInterface getMealPlanningDataAccess() { + return mealPlanningDataAccessInterface; + } + + /** + * Build the main frame. + * @return The main frame + */ + public JFrame build() { + return frontPageView; + } +} \ No newline at end of file diff --git a/src/main/java/data_access/DBNoteDataAccessObject.java b/src/main/java/data_access/DBNoteDataAccessObject.java deleted file mode 100644 index dadb0cab0..000000000 --- a/src/main/java/data_access/DBNoteDataAccessObject.java +++ /dev/null @@ -1,107 +0,0 @@ -package data_access; - -import java.io.IOException; - -import org.json.JSONException; -import org.json.JSONObject; - -import entity.User; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import use_case.note.DataAccessException; -import use_case.note.NoteDataAccessInterface; - -/** - * The DAO for accessing notes stored in the database. - *

This class demonstrates how your group can use the password-protected user - * endpoints of the API used in lab 5 to store persistent data in your program. - *

- *

You can also refer to the lab 5 code for signing up a new user and other use cases. - *

- * See - * - * the documentation - * of the API for more details. - */ -public class DBNoteDataAccessObject implements NoteDataAccessInterface { - private static final int SUCCESS_CODE = 200; - private static final int CREDENTIAL_ERROR = 401; - private static final String CONTENT_TYPE_LABEL = "Content-Type"; - private static final String CONTENT_TYPE_JSON = "application/json"; - private static final String STATUS_CODE_LABEL = "status_code"; - private static final String USERNAME = "username"; - private static final String PASSWORD = "password"; - private static final String MESSAGE = "message"; - - @Override - public String saveNote(User user, String note) throws DataAccessException { - final OkHttpClient client = new OkHttpClient().newBuilder() - .build(); - - // POST METHOD - final MediaType mediaType = MediaType.parse(CONTENT_TYPE_JSON); - final JSONObject requestBody = new JSONObject(); - requestBody.put(USERNAME, user.getName()); - requestBody.put(PASSWORD, user.getPassword()); - final JSONObject extra = new JSONObject(); - extra.put("note", note); - requestBody.put("info", extra); - final RequestBody body = RequestBody.create(requestBody.toString(), mediaType); - final Request request = new Request.Builder() - .url("http://vm003.teach.cs.toronto.edu:20112/modifyUserInfo") - .method("PUT", body) - .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_JSON) - .build(); - try { - final Response response = client.newCall(request).execute(); - - final JSONObject responseBody = new JSONObject(response.body().string()); - - if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) { - return loadNote(user); - } - else if (responseBody.getInt(STATUS_CODE_LABEL) == CREDENTIAL_ERROR) { - throw new DataAccessException("message could not be found or password was incorrect"); - } - else { - throw new DataAccessException("database error: " + responseBody.getString(MESSAGE)); - } - } - catch (IOException | JSONException ex) { - throw new DataAccessException(ex.getMessage()); - } - } - - @Override - public String loadNote(User user) throws DataAccessException { - // Make an API call to get the user object. - final String username = user.getName(); - final OkHttpClient client = new OkHttpClient().newBuilder().build(); - final Request request = new Request.Builder() - .url(String.format("http://vm003.teach.cs.toronto.edu:20112/user?username=%s", username)) - .addHeader("Content-Type", CONTENT_TYPE_JSON) - .build(); - try { - final Response response = client.newCall(request).execute(); - - final JSONObject responseBody = new JSONObject(response.body().string()); - - if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) { - final JSONObject userJSONObject = responseBody.getJSONObject("user"); - final JSONObject data = userJSONObject.getJSONObject("info"); - return data.getString("note"); - } - else { - throw new DataAccessException(responseBody.getString(MESSAGE)); - } - } - catch (IOException | JSONException ex) { - throw new RuntimeException(ex); - } - } -} diff --git a/src/main/java/data_access/JSONArrayTypeAdapter.java b/src/main/java/data_access/JSONArrayTypeAdapter.java new file mode 100644 index 000000000..5300f27e0 --- /dev/null +++ b/src/main/java/data_access/JSONArrayTypeAdapter.java @@ -0,0 +1,52 @@ +package data_access; + +import java.lang.reflect.Type; + +import org.json.JSONArray; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * Custom Gson type adapter for serializing and deserializing objects. + */ +public class JSONArrayTypeAdapter implements JsonDeserializer, JsonSerializer { + + /** + * Serializes an org.json.JSONArray into a Gson JsonArray. + * + * @param src the source {@link JSONArray} to serialize + * @param typeOfSrc the specific genericized type of the source object + * @param context the context of the serialization process + * @return a {@link JsonElement} representing the serialized {@link JSONArray} + */ + @Override + public JsonElement serialize(JSONArray src, Type typeOfSrc, JsonSerializationContext context) { + final JsonArray jsonArray = new JsonArray(); + for (int i = 0; i < src.length(); i++) { + jsonArray.add(src.get(i).toString()); + } + return jsonArray; + } + + /** + * Deserializes a Gson JsonElement into an org.json.JSONArray. + * + * @param json the JSON data being deserialized + * @param typeOfT the specific generalized type of the desired object + * @param context the context of the deserialization process + * @return a {@link JSONArray} representing the deserialized data + */ + @Override + public JSONArray deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + final JSONArray jsonArray = new JSONArray(); + for (JsonElement element : json.getAsJsonArray()) { + jsonArray.put(element.getAsString()); + } + return jsonArray; + } +} diff --git a/src/main/java/data_access/LocalDateAdapter.java b/src/main/java/data_access/LocalDateAdapter.java new file mode 100644 index 000000000..fdf22f3ce --- /dev/null +++ b/src/main/java/data_access/LocalDateAdapter.java @@ -0,0 +1,43 @@ +package data_access; + +import java.lang.reflect.Type; +import java.time.LocalDate; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * A custom Gson adapter for serializing and deserializing objects. + */ +public class LocalDateAdapter implements JsonDeserializer, JsonSerializer { + + /** + * Serializes LocalDate object into a JSON string. + * + * @param date the {@link LocalDate} to serialize + * @param typeOfSrc the type of the source object + * @param context the context of the serialization process + * @return a {@link JsonElement} representing the serialized {@link LocalDate} + */ + @Override + public JsonElement serialize(LocalDate date, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(date.toString()); + } + + /** + * Deserializes a JSON string into a LocalDate object. + * + * @param json the JSON element to deserialize + * @param typeOfT the type of the target object + * @param context the context of the deserialization process + * @return a {@link LocalDate} parsed from the JSON string + */ + @Override + public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return LocalDate.parse(json.getAsString()); + } +} diff --git a/src/main/java/data_access/MealPlanningDataAccessObject.java b/src/main/java/data_access/MealPlanningDataAccessObject.java new file mode 100644 index 000000000..50c90ea10 --- /dev/null +++ b/src/main/java/data_access/MealPlanningDataAccessObject.java @@ -0,0 +1,134 @@ +package data_access; + +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.json.JSONArray; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import entity.MealPlanEntry; +import entity.Recipe; +import use_case.meal_planning.MealPlanningDataAccessInterface; + +/** + * Implementation of MealPlanningDataAccessInterface that manages meal plan entries using JSON file storage. + */ +public class MealPlanningDataAccessObject implements MealPlanningDataAccessInterface { + + private final Map mealPlanEntries = new HashMap<>(); + private final String filePath = "meal_plan.json"; + private final SavedRecipesDataAccessInterface savedRecipesDataAccessInterface; + private int nextEntryId = 1; + + /** + * Constructs a new MealPlanningDataAccessObject. + * + * @param savedRecipesDataAccessInterface the data access object for saved recipes + */ + public MealPlanningDataAccessObject(SavedRecipesDataAccessInterface savedRecipesDataAccessInterface) { + this.savedRecipesDataAccessInterface = savedRecipesDataAccessInterface; + loadFromJsonFile(); + } + + @Override + public MealPlanEntry getMealPlanEntry(int userId, int entryId) { + final MealPlanEntry entry = mealPlanEntries.get(entryId); + final MealPlanEntry result; + if (entry != null && entry.getUserId() == userId) { + result = entry; + } + else { + result = null; + } + return result; + } + + @Override + public void updateMealStatus(int userId, int entryId, String status) { + final MealPlanEntry entry = mealPlanEntries.get(entryId); + if (entry != null && entry.getUserId() == userId) { + entry.setStatus(status); + saveToJsonFile(); + } + else { + throw new IllegalArgumentException("Entry not found or unauthorized access"); + } + } + + @Override + public void addMealPlanEntry(int userId, int recipeId, LocalDate date, String mealType) { + final Recipe recipe = savedRecipesDataAccessInterface.getSavedRecipe(userId, recipeId); + if (recipe == null) { + throw new IllegalArgumentException("Recipe not found in saved recipes"); + } + + final MealPlanEntry entry = new MealPlanEntry(nextEntryId++, recipe, date, userId, mealType); + mealPlanEntries.put(entry.getEntryId(), entry); + saveToJsonFile(); + } + + @Override + public void removeMealPlanEntry(int userId, int mealPlanEntryId) { + final MealPlanEntry entry = mealPlanEntries.get(mealPlanEntryId); + if (entry != null && entry.getUserId() == userId) { + mealPlanEntries.remove(mealPlanEntryId); + saveToJsonFile(); + } + } + + @Override + public List getWeeklyPlan(int userId, LocalDate weekStart) { + final int daysInWeek = 7; + return mealPlanEntries.values().stream() + .filter(entry -> entry.getUserId() == userId) + .filter(entry -> { + final LocalDate date = entry.getDate(); + return !date.isBefore(weekStart) && date.isBefore(weekStart.plusDays(daysInWeek)); + }) + .toList(); + } + + private void saveToJsonFile() { + try (Writer writer = new FileWriter(filePath)) { + final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .registerTypeAdapter(JSONArray.class, new JSONArrayTypeAdapter()) + .create(); + gson.toJson(mealPlanEntries, writer); + } + catch (IOException exception) { + System.err.println("Error saving to JSON file: " + exception.getMessage()); + } + } + + private void loadFromJsonFile() { + try (Reader reader = new FileReader(filePath)) { + final Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .registerTypeAdapter(JSONArray.class, new JSONArrayTypeAdapter()) + .create(); + + final Type type = new TypeToken>() { }.getType(); + final Map loadedEntries = gson.fromJson(reader, type); + + if (loadedEntries != null) { + mealPlanEntries.putAll(loadedEntries); + nextEntryId = loadedEntries.keySet().stream().max(Integer::compareTo).orElse(0) + 1; + } + } + catch (IOException exception) { + System.err.println("Error loading from JSON file: " + exception.getMessage()); + } + } +} diff --git a/src/main/java/data_access/NutritionAnalysisDataAccessObject.java b/src/main/java/data_access/NutritionAnalysisDataAccessObject.java new file mode 100644 index 000000000..3e441d746 --- /dev/null +++ b/src/main/java/data_access/NutritionAnalysisDataAccessObject.java @@ -0,0 +1,99 @@ +package data_access; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.json.JSONException; +import org.json.JSONObject; + +import entity.Nutrient; +import entity.Recipe; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import use_case.nutrition_analysis.NutritionAnalysisDataAccessInterface; +import use_case.nutrition_analysis.NutritionAnalysisException; + +/** + * The DAO that implements the interface. + *

+ * Contains the main method of the interface, which extract information from an api. + * Contains several helper methods. + *

+ */ +public class NutritionAnalysisDataAccessObject implements NutritionAnalysisDataAccessInterface { + private static final String APP_ID = "6ec3d1f4"; + private static final String APP_KEY = "0bae38670d4f3777d686730b59f0e707"; + private static final String NA_URL = "https://api.edamam.com/api/nutrition-details"; + private final OkHttpClient httpClient = new OkHttpClient(); + + public NutritionAnalysisDataAccessObject() { + // Empty constructor + } + + @Override + public List analyzeNutrition(Recipe recipe) throws IOException { + List nutrientsList = new ArrayList<>(); + + // Build the HTTP request + final Request request = buildNutritionRequest(recipe); + + // Process the response + try (Response response = httpClient.newCall(request).execute()) { + if (response.body() != null) { + nutrientsList = parseNutrientsResponse(response.body().string()); + } + } + catch (IOException exception) { + throw new IOException("Error processing nutrition analysis", exception); + } + + return nutrientsList; + } + + private Request buildNutritionRequest(Recipe recipe) { + final HttpUrl urlBuilder = Objects.requireNonNull(HttpUrl.parse(NA_URL)).newBuilder() + .addQueryParameter("app_id", APP_ID) + .addQueryParameter("app_key", APP_KEY) + .build(); + + final JSONObject requestBody = new JSONObject(); + requestBody.put("title", recipe.getTitle()); + requestBody.put("ingr", recipe.getJsonIngredient()); + + return new Request.Builder() + .url(urlBuilder) + .post(RequestBody.create( + requestBody.toString(), + MediaType.get("application/json; charset=utf-8") + )) + .build(); + } + + private List parseNutrientsResponse(String jsonData) throws NutritionAnalysisException { + final List nutrientsList = new ArrayList<>(); + try { + final JSONObject jsonObject = new JSONObject(jsonData); + final JSONObject totalNutrients = jsonObject.getJSONObject("totalNutrients"); + + for (String key : totalNutrients.keySet()) { + final JSONObject nutrient = totalNutrients.getJSONObject(key); + final String aNutrient = nutrient.getString("label") + + ": " + + nutrient.getInt("quantity") + + nutrient.getString("unit"); + + nutrientsList.add(new Nutrient(aNutrient)); + } + } + catch (JSONException exception) { + throw new NutritionAnalysisException("Error processing nutrition analysis", exception); + } + return nutrientsList; + } +} diff --git a/src/main/java/data_access/RecipeSearchDataAccessObject.java b/src/main/java/data_access/RecipeSearchDataAccessObject.java new file mode 100644 index 000000000..e3c04231a --- /dev/null +++ b/src/main/java/data_access/RecipeSearchDataAccessObject.java @@ -0,0 +1,173 @@ +package data_access; + +import entity.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; +import use_case.recipe_search.RecipeSearchDataAccessInterface; + +public class RecipeSearchDataAccessObject implements RecipeSearchDataAccessInterface { + private static final String APP_ID = "aaac3ad0"; + private static final String APP_KEY = "3862f2800864a629947f0f79f4da280d"; + private static final String BASE_URL = "https://api.edamam.com/api/recipes/v2"; + private static final String NA_URL = "https://api.edamam.com/api/nutrition-details"; + private final OkHttpClient httpClient = new OkHttpClient(); + + @Override + public List searchRecipesByFoodName(String foodName) { + return searchRecipes(foodName, null, null, null); + } + + /** + * Fetch recipes from Edamam API. + */ + private List searchRecipes(String foodName, String diet, String health, String cuisineType) { + List recipes = new ArrayList<>(); + + // Build API URL + HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL).newBuilder(); + urlBuilder.addQueryParameter("type", "public"); + urlBuilder.addQueryParameter("q", foodName); + urlBuilder.addQueryParameter("app_id", APP_ID); + urlBuilder.addQueryParameter("app_key", APP_KEY); + + // Add optional filters if provided + if (diet != null && !diet.isEmpty()) { + urlBuilder.addQueryParameter("diet", diet); + } + if (health != null && !health.isEmpty()) { + urlBuilder.addQueryParameter("health", health); + } + if (cuisineType != null && !cuisineType.isEmpty()) { + urlBuilder.addQueryParameter("cuisineType", cuisineType); + } + + Request request = new Request.Builder().url(urlBuilder.build().toString()).build(); + + // API Call + try (Response response = httpClient.newCall(request).execute()) { + if (response.body() != null) { + String jsonData = response.body().string(); + JSONObject jsonObject = new JSONObject(jsonData); + JSONArray hitsArray = jsonObject.getJSONArray("hits"); + + for (int i = 0; i < hitsArray.length(); i++) { + JSONObject recipeJson = hitsArray.getJSONObject(i).getJSONObject("recipe"); + + // Extract details + String name = recipeJson.getString("label"); + JSONArray jsonIngredient = recipeJson.getJSONArray("ingredientLines"); + String description = recipeJson.optString("source", "No description available"); + String instructions = recipeJson.optString("url", "Instructions not available"); + List ingredients = extractIngredients(recipeJson.getJSONArray("ingredients")); + + // Provide default nutrition if not available + Nutrition nutrition = new Nutrition(0, 0, 0, 0, 0, 0); + + // Add a default food list and servings + List food = new ArrayList<>(); + int servings = 1; + + // Create Recipe + Recipe recipe = new Recipe( + i + 1, + name, + description, + ingredients, + instructions, + nutrition, + food, + jsonIngredient, + servings + ); + + recipes.add(recipe); + } + } + } catch (IOException e) { + throw new RuntimeException(); + } + return recipes; + } + + /** + * Extract detailed ingredients from a JSON array. + */ + private List extractIngredients(JSONArray ingredientsArray) { + List ingredients = new ArrayList<>(); + for (int i = 0; i < ingredientsArray.length(); i++) { + JSONObject ingredientJson = ingredientsArray.getJSONObject(i); + + String food = ingredientJson.getString("food"); + double quantity = ingredientJson.optDouble("quantity", 0.0); + String unit = ingredientJson.optString("measure", ""); + + ingredients.add(new Ingredient(i + 1, food, quantity, unit)); + } + return ingredients; + } + + /** + * Retrieve detailed recipe information by recipe ID. + */ + public Recipe getRecipeById(String recipeId) { + HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL).newBuilder(); + urlBuilder.addQueryParameter("type", "public"); + urlBuilder.addQueryParameter("id", recipeId); + urlBuilder.addQueryParameter("app_id", APP_ID); + urlBuilder.addQueryParameter("app_key", APP_KEY); + + Request request = new Request.Builder().url(urlBuilder.build().toString()).build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (response.body() != null) { + String jsonData = response.body().string(); + JSONObject jsonObject = new JSONObject(jsonData); + JSONObject recipeJson = jsonObject.getJSONObject("recipe"); + + // Extract details + String name = recipeJson.getString("label"); + String description = recipeJson.optString("source", "No description available"); + String instructions = recipeJson.optString("url", "Instructions not available"); + List ingredients = extractIngredients(recipeJson.getJSONArray("ingredients")); + + // Extract nutrition + JSONObject nutritionJson = recipeJson.optJSONObject("totalNutrients"); + Nutrition nutrition = (nutritionJson != null) ? new Nutrition( + nutritionJson.optJSONObject("ENERC_KCAL").optDouble("quantity", 0), + nutritionJson.optJSONObject("PROCNT").optDouble("quantity", 0), + nutritionJson.optJSONObject("FAT").optDouble("quantity", 0), + nutritionJson.optJSONObject("CHOCDF").optDouble("quantity", 0), + nutritionJson.optJSONObject("FIBTG").optDouble("quantity", 0), + nutritionJson.optJSONObject("SUGAR").optDouble("quantity", 0) + ) : new Nutrition(0, 0, 0, 0, 0, 0); + + List food = new ArrayList<>(); + int servings = 1; + + return new Recipe( + Integer.parseInt(recipeId), + name, + description, + ingredients, + instructions, + nutrition, + food, + new JSONArray(), + servings + + ); + } + } catch (IOException e) { + throw new RuntimeException(); + } + return null; + } +} diff --git a/src/main/java/data_access/SavedRecipesDataAccessInterface.java b/src/main/java/data_access/SavedRecipesDataAccessInterface.java new file mode 100644 index 000000000..0b5694476 --- /dev/null +++ b/src/main/java/data_access/SavedRecipesDataAccessInterface.java @@ -0,0 +1,11 @@ +package data_access; + +import entity.Recipe; +import java.util.*; + +public interface SavedRecipesDataAccessInterface { + void saveRecipe(int userId, Recipe recipe); + void removeRecipe(int userId, int recipeId); + List getSavedRecipes(int userId); + Recipe getSavedRecipe(int userId, int recipeId); +} diff --git a/src/main/java/data_access/SavedRecipesDataAccessObject.java b/src/main/java/data_access/SavedRecipesDataAccessObject.java new file mode 100644 index 000000000..85a168c6c --- /dev/null +++ b/src/main/java/data_access/SavedRecipesDataAccessObject.java @@ -0,0 +1,55 @@ +package data_access; + +import entity.Recipe; +import java.util.*; + +public class SavedRecipesDataAccessObject implements SavedRecipesDataAccessInterface { + private final Map> userRecipes = new HashMap<>(); + + public SavedRecipesDataAccessObject() { + // Empty constructor + } + + @Override + public void saveRecipe(int userId, Recipe recipe) { + if (recipe == null) { + throw new IllegalArgumentException("Recipe cannot be null"); + } + userRecipes.computeIfAbsent(userId, k -> new HashMap<>()) + .put(recipe.getRecipeId(), recipe); + } + + @Override + public void removeRecipe(int userId, int recipeId) { + Map userRecipeMap = userRecipes.get(userId); + if (userRecipeMap != null) { + Recipe removedRecipe = userRecipeMap.remove(recipeId); + if (userRecipeMap.isEmpty()) { + userRecipes.remove(userId); + } + if (removedRecipe == null) { + throw new IllegalArgumentException("Recipe not found for user"); + } + } else { + throw new IllegalArgumentException("No recipes found for user"); + } + } + + @Override + public List getSavedRecipes(int userId) { + return new ArrayList<>(userRecipes.getOrDefault(userId, new HashMap<>()).values()); + } + + @Override + public Recipe getSavedRecipe(int userId, int recipeId) { + Map userRecipeMap = userRecipes.get(userId); + if (userRecipeMap == null) { + return null; + } + Recipe recipe = userRecipeMap.get(recipeId); + if (recipe == null) { + throw new IllegalArgumentException("Recipe not found"); + } + return recipe; + } +} \ No newline at end of file diff --git a/src/main/java/data_access/SearchWithRestrictionDataAccessObject.java b/src/main/java/data_access/SearchWithRestrictionDataAccessObject.java new file mode 100644 index 000000000..1e2ce254a --- /dev/null +++ b/src/main/java/data_access/SearchWithRestrictionDataAccessObject.java @@ -0,0 +1,116 @@ +package data_access; + +import entity.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; +import use_case.search_with_restriction.SearchWithRestrictionDataAccessInterface; + +public class SearchWithRestrictionDataAccessObject implements SearchWithRestrictionDataAccessInterface { + private static final String APP_ID = "aaac3ad0"; + private static final String APP_KEY = "3862f2800864a629947f0f79f4da280d"; + private static final String BASE_URL = "https://api.edamam.com/api/recipes/v2"; + private final OkHttpClient httpClient = new OkHttpClient(); + + @Override + public List searchRecipesByRestriction(String foodName, String diet, String health, String + cuisineType) { + return searchRecipes(foodName, diet, health, cuisineType); + } + + /** + * Fetch recipes from Edamam API. + */ + private List searchRecipes(String foodName, String diet, String health, String cuisineType) { + final List recipes = new ArrayList<>(); + + // Build API URL + final HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL).newBuilder(); + urlBuilder.addQueryParameter("type", "public"); + urlBuilder.addQueryParameter("q", foodName); + urlBuilder.addQueryParameter("app_id", APP_ID); + urlBuilder.addQueryParameter("app_key", APP_KEY); + + // Add optional filters if provided + if (diet != null && !diet.isEmpty()) { + urlBuilder.addQueryParameter("diet", diet); + } + if (health != null && !health.isEmpty()) { + urlBuilder.addQueryParameter("health", health); + } + if (cuisineType != null && !cuisineType.isEmpty()) { + urlBuilder.addQueryParameter("cuisineType", cuisineType); + } + + Request request = new Request.Builder().url(urlBuilder.build().toString()).build(); + + // API Call + try (Response response = httpClient.newCall(request).execute()) { + if (response.body() != null) { + String jsonData = response.body().string(); + JSONObject jsonObject = new JSONObject(jsonData); + JSONArray hitsArray = jsonObject.getJSONArray("hits"); + + for (int i = 0; i < hitsArray.length(); i++) { + JSONObject recipeJson = hitsArray.getJSONObject(i).getJSONObject("recipe"); + + // Extract details + String name = recipeJson.getString("label"); + JSONArray jsonIngredient = recipeJson.getJSONArray("ingredientLines"); + String description = recipeJson.optString("source", "No description available"); + String instructions = recipeJson.optString("url", "Instructions not available"); + List ingredients = extractIngredients(recipeJson.getJSONArray("ingredients")); + + // Provide default nutrition if not available + Nutrition nutrition = new Nutrition(0, 0, 0, 0, 0, 0); + + // Add a default food list and servings + List food = new ArrayList<>(); + int servings = 1; + + // Create Recipe + Recipe recipe = new Recipe( + i + 1, + name, + description, + ingredients, + instructions, + nutrition, + food, + jsonIngredient, + servings + ); + + recipes.add(recipe); + } + } + } catch (IOException e) { + throw new RuntimeException(); + } + return recipes; + } + + /** + * Extract detailed ingredients from a JSON array. + */ + private List extractIngredients(JSONArray ingredientsArray) { + List ingredients = new ArrayList<>(); + for (int i = 0; i < ingredientsArray.length(); i++) { + JSONObject ingredientJson = ingredientsArray.getJSONObject(i); + + String food = ingredientJson.getString("food"); + double quantity = ingredientJson.optDouble("quantity", 0.0); + String unit = ingredientJson.optString("measure", ""); + + ingredients.add(new Ingredient(i + 1, food, quantity, unit)); + } + return ingredients; + } +} diff --git a/src/main/java/entity/DietPlan.java b/src/main/java/entity/DietPlan.java new file mode 100644 index 000000000..eeaa43faa --- /dev/null +++ b/src/main/java/entity/DietPlan.java @@ -0,0 +1,65 @@ +package entity; + +import java.util.Date; +import java.util.List; + +/** + * The representation of user's dietary plan. + */ +public class DietPlan { + + private int planId; + private int userId; + private Date calendarDate; + private List recipeIds; + private String goals; + + public DietPlan(int planId, int userId, Date calendarDate, List recipeIds, String goals) { + this.planId = planId; + this.userId = userId; + this.calendarDate = calendarDate; + this.recipeIds = recipeIds; + this.goals = goals; + } + + public int getPlanId() { + return planId; + } + + public void setPlanId(int planId) { + this.planId = planId; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public Date getCalendarDate() { + return calendarDate; + } + + public void setCalendarDate(Date calendarDate) { + this.calendarDate = calendarDate; + } + + public List getRecipeIds() { + return recipeIds; + } + + public void setRecipeIds(List recipeIds) { + this.recipeIds = recipeIds; + } + + public String getGoals() { + return goals; + } + + public void setGoals(String goals) { + this.goals = goals; + } +} + diff --git a/src/main/java/entity/Food.java b/src/main/java/entity/Food.java new file mode 100644 index 000000000..8916bcc54 --- /dev/null +++ b/src/main/java/entity/Food.java @@ -0,0 +1,73 @@ +package entity; + +/** + * The class of food. + */ +public class Food { + + private int id; + private String name; + private String category; + private String unit; + private double servingSize; + + private Nutrition nutrition; + + public Food(int id, String name, String category, String unit, double servingSize, Nutrition nutrition) { + this.id = id; + this.name = name; + this.category = category; + this.unit = unit; + this.servingSize = servingSize; + this.nutrition = nutrition; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public double getServingSize() { + return servingSize; + } + + public void setServingSize(double servingSize) { + this.servingSize = servingSize; + } + + public Nutrition getNutrition() { + return nutrition; + } + + public void setNutrition(Nutrition nutrition) { + this.nutrition = nutrition; + } +} + diff --git a/src/main/java/entity/Ingredient.java b/src/main/java/entity/Ingredient.java new file mode 100644 index 000000000..c16bbfa22 --- /dev/null +++ b/src/main/java/entity/Ingredient.java @@ -0,0 +1,61 @@ +package entity; + +/** + * The class represents an ingredient in a recipe. + */ +public final class Ingredient { + + private final int ingredientId; + private final String name; + private double quantity; + private final String unit; + + /** + * Constructor for the Ingredient class. + * + * @param ingredientId The unique identifier of the ingredient. + * @param name The name of the ingredient. + * @param quantity The quantity of the ingredient. + * @param unit The unit of measurement for the ingredient. + * @throws IllegalArgumentException if quantity is negative or name/unit is null or empty. + */ + public Ingredient(int ingredientId, String name, double quantity, String unit) { + + this.ingredientId = ingredientId; + this.name = name; + this.quantity = quantity; + this.unit = unit; + } + + public int getIngredientId() { + return ingredientId; + } + + public String getName() { + return name; + } + + public double getQuantity() { + return quantity; + } + + public String getUnit() { + return unit; + } + + public void setQuantity(double quantity) { + this.quantity = quantity; + } + + /** + * Scales the ingredient quantity by a given factor. + * + * @param factor The scaling factor (e.g., 2.0 for doubling the quantity). + * @return A new Ingredient object with the scaled quantity. + * @throws IllegalArgumentException if factor is negative. + */ + public Ingredient scaleQuantity(double factor) { + return new Ingredient(ingredientId, name, quantity * factor, unit); + } + +} diff --git a/src/main/java/entity/MealPlanEntry.java b/src/main/java/entity/MealPlanEntry.java new file mode 100644 index 000000000..d15518c07 --- /dev/null +++ b/src/main/java/entity/MealPlanEntry.java @@ -0,0 +1,76 @@ +package entity; + +import java.time.LocalDate; + +/** + * The class represents meal plan object. + */ +public class MealPlanEntry { + private int entryId; + private Recipe recipe; + private LocalDate date; + private int userId; + private String mealType; + private String status; + // Added to track state like "planned", "completed", etc. + + public MealPlanEntry(int entryId, Recipe recipe, LocalDate date, int userId, String mealType) { + this.entryId = entryId; + this.recipe = recipe; + this.date = date; + this.userId = userId; + this.mealType = mealType; + this.status = "planned"; + // Default status + } + + // Getters and setters + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public int getEntryId() { + return entryId; + } + + public void setEntryId(int entryId) { + this.entryId = entryId; + } + + public Recipe getRecipe() { + return recipe; + } + + public void setRecipe(Recipe recipe) { + this.recipe = recipe; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getMealType() { + return mealType; + } + + public void setMealType(String mealType) { + this.mealType = mealType; + } +} diff --git a/src/main/java/entity/Nutrient.java b/src/main/java/entity/Nutrient.java new file mode 100644 index 000000000..b96687ccf --- /dev/null +++ b/src/main/java/entity/Nutrient.java @@ -0,0 +1,21 @@ +package entity; + +/** + * The class represents the NutritionInfo for food, in form of the details of each Nutrient. + */ +public class Nutrient { + + private String nutrientInfo; + + public Nutrient(String nutrients) { + this.nutrientInfo = nutrients; + } + + public String getNutrientInfo() { + return nutrientInfo; + } + + public void setNutrientInfo(String nutrients) { + this.nutrientInfo = nutrients; + } +} diff --git a/src/main/java/entity/Nutrition.java b/src/main/java/entity/Nutrition.java new file mode 100644 index 000000000..4faead20f --- /dev/null +++ b/src/main/java/entity/Nutrition.java @@ -0,0 +1,83 @@ +package entity; + +/** + * The class represents nutrition for food. + */ +public class Nutrition { + + private double calories; + private double protein; + private double fat; + private double carbohydrates; + private double fiber; + private double sugar; + + public Nutrition(double calories, double protein, double fat, double carbohydrates, + double fiber, double sugar) { + this.calories = calories; + this.protein = protein; + this.fat = fat; + this.carbohydrates = carbohydrates; + this.fiber = fiber; + this.sugar = sugar; + } + + /** + * A method that formats the Nutrition Info into the structure required (A String). + * @return String, a simple structure that illustrates all nutrition information. + */ + public String formatNutritionInfo() { + return String.format( + "Calories: %.2f, Protein: %.2f g, Fat: %.2f g, Carbohydrates: %.2f g, Fiber: %.2f g, Sugar: %.2f g", + calories, protein, fat, carbohydrates, fiber, sugar + ); + } + + public double getCalories() { + return calories; + } + + public void setCalories(double calories) { + this.calories = calories; + } + + public double getProtein() { + return protein; + } + + public void setProtein(double protein) { + this.protein = protein; + } + + public double getFat() { + return fat; + } + + public void setFat(double fat) { + this.fat = fat; + } + + public double getCarbohydrates() { + return carbohydrates; + } + + public void setCarbohydrates(double carbohydrates) { + this.carbohydrates = carbohydrates; + } + + public double getFiber() { + return fiber; + } + + public void setFiber(double fiber) { + this.fiber = fiber; + } + + public double getSugar() { + return sugar; + } + + public void setSugar(double sugar) { + this.sugar = sugar; + } +} diff --git a/src/main/java/entity/Recipe.java b/src/main/java/entity/Recipe.java new file mode 100644 index 000000000..bf729ff4b --- /dev/null +++ b/src/main/java/entity/Recipe.java @@ -0,0 +1,126 @@ +package entity; + +import java.util.List; +import java.util.Objects; + +import org.json.JSONArray; + +/** + * The unified Recipe entity containing all necessary details. + */ +public class Recipe { + + private int recipeId; + private String title; + private String description; + private List ingredients; + private String instructions; + private Nutrition nutrition; + private List food; + private JSONArray jsonIngredient; + private int servings; + + /** + * Constructor for Recipe with all fields. + * @param recipeId the Id of the recipe in the api. + * @param title the name of the recipe. + * @param description a brief description of this recipe. + * @param ingredients a list of all ingredients of this recipe. + * @param instructions the instruction of making this meal. + * @param nutrition the nutrition information of this recipe given by the api. + * @param food a list of all food required for this meal. + * @param jsonIngredient the list of ingredients in JSONArray form. + * @param servings the number of people this meal is serving. + */ + public Recipe(int recipeId, String title, String description, List ingredients, + String instructions, Nutrition nutrition, List food, JSONArray jsonIngredient, int servings) { + this.recipeId = recipeId; + this.title = title; + this.description = description; + this.ingredients = ingredients; + this.instructions = Objects.requireNonNullElse(instructions, "Instructions not available"); + this.nutrition = nutrition; + this.food = food; + this.jsonIngredient = jsonIngredient; + this.servings = servings; + } + + // Getters and setters for all fields + public int getRecipeId() { + return recipeId; + } + + public void setRecipeId(int recipeId) { + this.recipeId = recipeId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getIngredients() { + return ingredients; + } + + public void setIngredients(List ingredients) { + this.ingredients = ingredients; + } + + public String getInstructions() { + return instructions; + } + + public void setInstructions(String instructions) { + this.instructions = instructions; + } + + public Nutrition getNutrition() { + return nutrition; + } + + public void setNutrition(Nutrition nutrition) { + this.nutrition = nutrition; + } + + public List getFood() { + return food; + } + + public void setFood(List food) { + this.food = food; + } + + public JSONArray getJsonIngredient() { + return jsonIngredient; + } + + public void setJsonIngredient(JSONArray jsonIngredient) { + this.jsonIngredient = jsonIngredient; + } + + public int getServings() { + return servings; + } + + public void setServings(int servings) { + this.servings = servings; + } + + @Override + public String toString() { + return String.format("Recipe: %s (Servings: %d)%nDescription: %s%nIngredients: %s%nInstructions: %s", + title, servings, description, ingredients, instructions); + } +} diff --git a/src/main/java/entity/User.java b/src/main/java/entity/User.java index e0c57e9a6..a57b49ec5 100644 --- a/src/main/java/entity/User.java +++ b/src/main/java/entity/User.java @@ -1,24 +1,74 @@ package entity; +import java.util.List; + /** * The representation of a password-protected user for our program. */ public class User { - private final String name; - private final String password; + private int userId; + private String username; + private String password; + private String email; + private String preferences; + private List savedRecipes; + + public User(String username, String password) { - public User(String name, String password) { - this.name = name; + this.userId = 0; + this.username = username; this.password = password; + this.email = ""; + this.preferences = null; + this.savedRecipes = null; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; } public String getName() { - return name; + return username; + } + + public void setName(String name) { + this.username = name; } public String getPassword() { return password; } + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPreferences() { + return preferences; + } + + public void setPreferences(String preferences) { + this.preferences = preferences; + } + + public List getSavedRecipes() { + return savedRecipes; + } + + public void setSavedRecipes(List savedRecipes) { + this.savedRecipes = savedRecipes; + } } diff --git a/src/main/java/entity/UserFactory.java b/src/main/java/entity/UserFactory.java new file mode 100644 index 000000000..e367faeb2 --- /dev/null +++ b/src/main/java/entity/UserFactory.java @@ -0,0 +1,17 @@ +package entity; + +/** + * Factory for creating CommonUser objects. + */ +public class UserFactory { + + /** + * Creates a new User. + * @param name the name of the new user + * @param password the password of the new user + * @return the new user + */ + public User create(String name, String password) { + return new User(name, password); + } +} diff --git a/src/main/java/interface_adapter/meal_planning/MealPlanningController.java b/src/main/java/interface_adapter/meal_planning/MealPlanningController.java new file mode 100644 index 000000000..5b2ca66aa --- /dev/null +++ b/src/main/java/interface_adapter/meal_planning/MealPlanningController.java @@ -0,0 +1,86 @@ +package interface_adapter.meal_planning; + +import java.time.LocalDate; +import java.util.List; + +import entity.Recipe; +import use_case.meal_planning.MealPlanningInputBoundary; + +/** + * Controller class for managing meal planning operations. + * Acts as an interface between the UI and the meal planning use case. + */ +public class MealPlanningController { + private final MealPlanningInputBoundary mealPlanningInputBoundaryUseCase; + + /** + * Constructs a new MealPlanningController. + * + * @param mealPlanningInputBoundaryUseCase the meal planning use case to be controlled + */ + public MealPlanningController(MealPlanningInputBoundary mealPlanningInputBoundaryUseCase) { + this.mealPlanningInputBoundaryUseCase = mealPlanningInputBoundaryUseCase; + } + + /** + * Retrieves all saved recipes for a user. + * + * @param userId the ID of the user + * @return list of saved recipes + */ + public List getSavedRecipes(int userId) { + return mealPlanningInputBoundaryUseCase.getSavedRecipes(userId); + } + + /** + * Updates the status of a meal plan entry. + * + * @param userId the ID of the user + * @param entryId the ID of the meal plan entry + * @param status the new status to set + */ + public void updateMealStatus(int userId, int entryId, String status) { + mealPlanningInputBoundaryUseCase.updateMealStatus(userId, entryId, status); + } + + /** + * Adds a recipe to the meal calendar. + * + * @param userId the ID of the user + * @param recipeId the ID of the recipe to add + * @param date the date to add the meal + * @param mealType the type of meal (e.g., breakfast, lunch, dinner) + */ + public void addToCalendar(int userId, int recipeId, LocalDate date, String mealType) { + mealPlanningInputBoundaryUseCase.addToCalendar(userId, recipeId, date, mealType); + } + + /** + * Removes a meal plan entry from the calendar. + * + * @param userId the ID of the user + * @param mealPlanEntryId the ID of the meal plan entry to remove + */ + public void removeFromCalendar(int userId, int mealPlanEntryId) { + mealPlanningInputBoundaryUseCase.removeFromCalendar(userId, mealPlanEntryId); + } + + /** + * Retrieves meal plan entries for a specific week. + * + * @param userId the ID of the user + * @param weekStart the start date of the week + */ + public void viewCalendarWeek(int userId, LocalDate weekStart) { + mealPlanningInputBoundaryUseCase.getCalendarWeek(userId, weekStart); + } + + /** + * Initializes meal planning for a user. + * + * @param userId the ID of the user + */ + public void initializeMealPlanning(int userId) { + mealPlanningInputBoundaryUseCase.initializeMealPlanning(userId); + } +} diff --git a/src/main/java/interface_adapter/meal_planning/MealPlanningPresenter.java b/src/main/java/interface_adapter/meal_planning/MealPlanningPresenter.java new file mode 100644 index 000000000..f7bd30c8c --- /dev/null +++ b/src/main/java/interface_adapter/meal_planning/MealPlanningPresenter.java @@ -0,0 +1,76 @@ +package interface_adapter.meal_planning; + +import java.util.List; + +import entity.MealPlanEntry; +import entity.Recipe; +import use_case.meal_planning.MealPlanningOutputBoundary; + +/** + * Presenter class for meal planning that implements the MealPlanningOutputBoundary interface. + * Handles updating the view model with data and state changes. + * + * @see MealPlanningOutputBoundary + * @see MealPlanningViewModelInterface + */ +public class MealPlanningPresenter implements MealPlanningOutputBoundary { + private final MealPlanningViewModelInterface viewModel; + + public MealPlanningPresenter(MealPlanningViewModelInterface viewModel) { + this.viewModel = viewModel; + } + + @Override + public void presentCalendarWeek(List entries) { + final MealPlanningState currentState = viewModel.getState(); + final MealPlanningState newState = new MealPlanningState(currentState); + newState.setMealPlanEntries(entries); + viewModel.setState(newState); + viewModel.firePropertyChanged(); + } + + @Override + public void presentAddSuccess(String message) { + final MealPlanningState state = viewModel.getState(); + final MealPlanningState newState = new MealPlanningState(state); + newState.setMessage(message); + viewModel.setState(newState); + viewModel.firePropertyChanged(); + } + + @Override + public void presentRemoveSuccess(String message) { + final MealPlanningState state = viewModel.getState(); + final MealPlanningState newState = new MealPlanningState(state); + newState.setMessage(message); + viewModel.setState(newState); + viewModel.firePropertyChanged(); + } + + @Override + public void presentStatusUpdateSuccess(String message) { + final MealPlanningState state = viewModel.getState(); + final MealPlanningState newState = new MealPlanningState(state); + newState.setMessage(message); + viewModel.setState(newState); + viewModel.firePropertyChanged(); + } + + @Override + public void presentError(String error) { + final MealPlanningState state = viewModel.getState(); + final MealPlanningState newState = new MealPlanningState(state); + newState.setError(error); + viewModel.setState(newState); + viewModel.firePropertyChanged(); + } + + @Override + public void presentSavedRecipes(List recipes) { + final MealPlanningState currentState = viewModel.getState(); + final MealPlanningState newState = new MealPlanningState(currentState); + newState.setSavedRecipes(recipes); + viewModel.setState(newState); + viewModel.firePropertyChanged(); + } +} diff --git a/src/main/java/interface_adapter/meal_planning/MealPlanningState.java b/src/main/java/interface_adapter/meal_planning/MealPlanningState.java new file mode 100644 index 000000000..7f68c2302 --- /dev/null +++ b/src/main/java/interface_adapter/meal_planning/MealPlanningState.java @@ -0,0 +1,100 @@ +package interface_adapter.meal_planning; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import entity.MealPlanEntry; +import entity.Recipe; + +/** + * Represents the state of meal planning functionality. + * Maintains lists of meal plan entries and saved recipes, along with current week, + * status messages, error messages, and loading state. + * Implements defensive copying for collections to maintain immutability. + * + * @see MealPlanningStateInterface + */ +public class MealPlanningState implements MealPlanningStateInterface { + private List mealPlanEntries = new ArrayList<>(); + private List savedRecipes = new ArrayList<>(); + private LocalDate currentWeekStart = LocalDate.now(); + private String message; + private String error; + private boolean isLoading; + + // Copy constructor + public MealPlanningState(MealPlanningState copy) { + if (copy != null) { + this.mealPlanEntries = new ArrayList<>(copy.mealPlanEntries); + this.savedRecipes = new ArrayList<>(copy.savedRecipes); + this.currentWeekStart = copy.currentWeekStart; + this.message = copy.message; + this.error = copy.error; + this.isLoading = copy.isLoading; + } + } + + // Default constructor + public MealPlanningState() { + + } + + @Override + public List getSavedRecipes() { + return new ArrayList<>(savedRecipes); + } + + @Override + public void setSavedRecipes(List recipes) { + this.savedRecipes = new ArrayList<>(recipes); + } + + @Override + public List getMealPlanEntries() { + return new ArrayList<>(mealPlanEntries); + } + + @Override + public void setMealPlanEntries(List entries) { + this.mealPlanEntries = new ArrayList<>(entries); + } + + @Override + public LocalDate getCurrentWeekStart() { + return currentWeekStart; + } + + @Override + public void setCurrentWeekStart(LocalDate date) { + this.currentWeekStart = date; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public void setMessage(String message) { + this.message = message; + } + + @Override + public String getError() { + return error; + } + + @Override + public void setError(String error) { + this.error = error; + } + + public boolean isLoading() { + return isLoading; + } + + public void setLoading(boolean loading) { + isLoading = loading; + } +} diff --git a/src/main/java/interface_adapter/meal_planning/MealPlanningStateInterface.java b/src/main/java/interface_adapter/meal_planning/MealPlanningStateInterface.java new file mode 100644 index 000000000..5cbe0ca72 --- /dev/null +++ b/src/main/java/interface_adapter/meal_planning/MealPlanningStateInterface.java @@ -0,0 +1,84 @@ +package interface_adapter.meal_planning; + +import java.time.LocalDate; +import java.util.List; + +import entity.MealPlanEntry; +import entity.Recipe; + +/** + * Interface defining the state management operations for meal planning. + * Provides methods to access and modify meal plan entries, recipes, dates, and status messages. + */ +public interface MealPlanningStateInterface { + + /** + * Gets the list of meal plan entries. + * + * @return list of meal plan entries + */ + List getMealPlanEntries(); + + /** + * Sets the list of meal plan entries. + * + * @param entries list of meal plan entries to set + */ + void setMealPlanEntries(List entries); + + /** + * Gets the list of saved recipes. + * + * @return list of saved recipes + */ + List getSavedRecipes(); + + /** + * Sets the list of saved recipes. + * + * @param recipes list of recipes to set + */ + void setSavedRecipes(List recipes); + + /** + * Gets the start date of the current week. + * + * @return current week start date + */ + LocalDate getCurrentWeekStart(); + + /** + * Sets the start date of the current week. + * + * @param date week start date to set + */ + void setCurrentWeekStart(LocalDate date); + + /** + * Gets the current status message. + * + * @return current status message + */ + String getMessage(); + + /** + * Sets the status message. + * + * @param message status message to set + */ + void setMessage(String message); + + /** + * Gets the current error message. + * + * @return current error message + */ + String getError(); + + /** + * Sets the error message. + * + * @param error error message to set + */ + void setError(String error); +} diff --git a/src/main/java/interface_adapter/meal_planning/MealPlanningViewModel.java b/src/main/java/interface_adapter/meal_planning/MealPlanningViewModel.java new file mode 100644 index 000000000..0ceb27cd9 --- /dev/null +++ b/src/main/java/interface_adapter/meal_planning/MealPlanningViewModel.java @@ -0,0 +1,33 @@ +package interface_adapter.meal_planning; + +import interface_adapter.ViewModel; + +/** + * View model for meal planning functionality that extends ViewModel and implements MealPlanningViewModelInterface. + * Manages the UI state and labels for the meal planning calendar view. + * + * @see ViewModel + * @see MealPlanningViewModelInterface + */ +public class MealPlanningViewModel extends ViewModel implements MealPlanningViewModelInterface { + public static final String TITLE_LABEL = "Meal Planning Calendar"; + + public MealPlanningViewModel() { + super(TITLE_LABEL); + } + + @Override + public MealPlanningState getState() { + return super.getState(); + } + + @Override + public void setState(MealPlanningState state) { + super.setState(state); + } + + @Override + public void firePropertyChanged() { + super.firePropertyChanged(); + } +} diff --git a/src/main/java/interface_adapter/meal_planning/MealPlanningViewModelInterface.java b/src/main/java/interface_adapter/meal_planning/MealPlanningViewModelInterface.java new file mode 100644 index 000000000..4dc08a7d1 --- /dev/null +++ b/src/main/java/interface_adapter/meal_planning/MealPlanningViewModelInterface.java @@ -0,0 +1,28 @@ +package interface_adapter.meal_planning; + +/** + * Interface for the meal planning view model that defines core state management operations. + * Provides methods to get/set state and notify of property changes. + */ +public interface MealPlanningViewModelInterface { + + /** + * Gets the current meal planning state. + * + * @return the current MealPlanningState + */ + MealPlanningState getState(); + + /** + * Sets a new meal planning state. + * + * @param state the new MealPlanningState to set + */ + void setState(MealPlanningState state); + + /** + * Notifies observers that the state has changed. + */ + void firePropertyChanged(); + +} diff --git a/src/main/java/interface_adapter/note/NoteController.java b/src/main/java/interface_adapter/note/NoteController.java deleted file mode 100644 index e3e5dfb32..000000000 --- a/src/main/java/interface_adapter/note/NoteController.java +++ /dev/null @@ -1,28 +0,0 @@ -package interface_adapter.note; - -import use_case.note.NoteInputBoundary; - -/** - * Controller for our Note related Use Cases. - */ -public class NoteController { - - private final NoteInputBoundary noteInteractor; - - public NoteController(NoteInputBoundary noteInteractor) { - this.noteInteractor = noteInteractor; - } - - /** - * Executes the Note related Use Cases. - * @param note the note to be recorded - */ - public void execute(String note) { - if (note != null) { - noteInteractor.executeSave(note); - } - else { - noteInteractor.executeRefresh(); - } - } -} diff --git a/src/main/java/interface_adapter/note/NotePresenter.java b/src/main/java/interface_adapter/note/NotePresenter.java deleted file mode 100644 index d4e416165..000000000 --- a/src/main/java/interface_adapter/note/NotePresenter.java +++ /dev/null @@ -1,38 +0,0 @@ -package interface_adapter.note; - -import use_case.note.NoteOutputBoundary; - -/** - * The presenter for our Note viewing and editing program. - */ -public class NotePresenter implements NoteOutputBoundary { - - private final NoteViewModel noteViewModel; - - public NotePresenter(NoteViewModel noteViewModel) { - this.noteViewModel = noteViewModel; - } - - /** - * Prepares the success view for the Note related Use Cases. - * - * @param note the output data - */ - @Override - public void prepareSuccessView(String note) { - noteViewModel.getState().setNote(note); - noteViewModel.getState().setError(null); - noteViewModel.firePropertyChanged(); - } - - /** - * Prepares the failure view for the Note related Use Cases. - * - * @param errorMessage the explanation of the failure - */ - @Override - public void prepareFailView(String errorMessage) { - noteViewModel.getState().setError(errorMessage); - noteViewModel.firePropertyChanged(); - } -} diff --git a/src/main/java/interface_adapter/note/NoteState.java b/src/main/java/interface_adapter/note/NoteState.java deleted file mode 100644 index c5b2234d6..000000000 --- a/src/main/java/interface_adapter/note/NoteState.java +++ /dev/null @@ -1,26 +0,0 @@ -package interface_adapter.note; - -/** - * The State for a note. - *

For this example, a note is simplay a string.

- */ -public class NoteState { - private String note = ""; - private String error; - - public String getNote() { - return note; - } - - public void setNote(String note) { - this.note = note; - } - - public void setError(String errorMessage) { - this.error = errorMessage; - } - - public String getError() { - return error; - } -} diff --git a/src/main/java/interface_adapter/note/NoteViewModel.java b/src/main/java/interface_adapter/note/NoteViewModel.java deleted file mode 100644 index 6e185d0fa..000000000 --- a/src/main/java/interface_adapter/note/NoteViewModel.java +++ /dev/null @@ -1,13 +0,0 @@ -package interface_adapter.note; - -import interface_adapter.ViewModel; - -/** - * The ViewModel for the NoteView. - */ -public class NoteViewModel extends ViewModel { - public NoteViewModel() { - super("note"); - setState(new NoteState()); - } -} diff --git a/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisController.java b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisController.java new file mode 100644 index 000000000..5447f1de7 --- /dev/null +++ b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisController.java @@ -0,0 +1,24 @@ +package interface_adapter.nutrition_analysis; + +import entity.Recipe; +import use_case.nutrition_analysis.NutritionAnalysis; + +/** + * Controller for the Nutrition Analysis functionality. + */ +public class NutritionAnalysisController { + private final NutritionAnalysis nutritionAnalysisUseCase; + + public NutritionAnalysisController(NutritionAnalysis nutritionAnalysisUsecase) { + this.nutritionAnalysisUseCase = nutritionAnalysisUsecase; + } + + /** + * Execute a nutrition analysis with the given recipe name. + * @param recipe the recipe the user wants to analyze. + */ + public void executeAnalysis(Recipe recipe) { + nutritionAnalysisUseCase.analyzeNutrition(recipe); + } +} + diff --git a/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisPresenter.java b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisPresenter.java new file mode 100644 index 000000000..e6d4b8f7f --- /dev/null +++ b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisPresenter.java @@ -0,0 +1,40 @@ +package interface_adapter.nutrition_analysis; + +import java.util.ArrayList; +import java.util.List; + +import entity.Nutrient; +import use_case.nutrition_analysis.NutritionAnalysisOutputBoundary; + +/** + * Presenter for the Nutrition Analysis functionality. + */ +public class NutritionAnalysisPresenter implements NutritionAnalysisOutputBoundary { + private final NutritionAnalysisViewModel viewModel; + + public NutritionAnalysisPresenter(NutritionAnalysisViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void presentNutritionInfo(List NutritionInfo) { + final NutritionAnalysisState state = new NutritionAnalysisState(); + final List nutritionResults = new ArrayList<>(); + + for (Nutrient nutrient : NutritionInfo) { + nutritionResults.add(nutrient.getNutrientInfo()); + } + state.setNutritionResults(nutritionResults); + viewModel.setState(state); + viewModel.firePropertyChanged(); + } + + @Override + public void presentError(String error) { + final NutritionAnalysisState state = new NutritionAnalysisState(); + state.setError(error); + viewModel.setState(state); + viewModel.firePropertyChanged(); + + } +} diff --git a/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisState.java b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisState.java new file mode 100644 index 000000000..b7cd62497 --- /dev/null +++ b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisState.java @@ -0,0 +1,35 @@ +package interface_adapter.nutrition_analysis; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the state for Nutrition Analysis functionality. + */ +public class NutritionAnalysisState { + private List nutritionResults = new ArrayList<>(); + private String error; + + /** + * Default constructor for NutritionAnalysisState. + */ + public NutritionAnalysisState() { + + } + + public List getNutritionResults() { + return nutritionResults; + } + + public String getError() { + return error; + } + + public void setNutritionResults(List nutritionResults) { + this.nutritionResults = nutritionResults; + } + + public void setError(String error) { + this.error = error; + } +} diff --git a/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisViewModel.java b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisViewModel.java new file mode 100644 index 000000000..f6e8a569b --- /dev/null +++ b/src/main/java/interface_adapter/nutrition_analysis/NutritionAnalysisViewModel.java @@ -0,0 +1,13 @@ +package interface_adapter.nutrition_analysis; + +import interface_adapter.ViewModel; + +/** + * The ViewModel for the NutritionAnalysis View. + */ +public class NutritionAnalysisViewModel extends ViewModel { + + public NutritionAnalysisViewModel() { + super("Nutrition Analysis"); + } +} diff --git a/src/main/java/interface_adapter/recipe_search/RecipeSearchController.java b/src/main/java/interface_adapter/recipe_search/RecipeSearchController.java new file mode 100644 index 000000000..bbcbb9440 --- /dev/null +++ b/src/main/java/interface_adapter/recipe_search/RecipeSearchController.java @@ -0,0 +1,44 @@ +package interface_adapter.recipe_search; + +import java.util.List; + +import entity.Recipe; +import use_case.recipe_search.RecipeSearchInputBoundary; + +/** + * Controller for the recipe search functionality. + */ +public class RecipeSearchController { + private final RecipeSearchInputBoundary recipeSearchInputBoundaryUseCase; + + public RecipeSearchController(RecipeSearchInputBoundary recipeSearchInputBoundaryUseCase) { + this.recipeSearchInputBoundaryUseCase = recipeSearchInputBoundaryUseCase; + } + + /** + * Execute a recipe search with the given ingredients. + * @param ingredients List of ingredients to search for + */ + public void executeSearch(List ingredients) { + try { + recipeSearchInputBoundaryUseCase.searchRecipes(ingredients); + } + catch (Exception e) { + // Error will be handled by the presenter through output boundary + } + } + + /** + * Execute a recipe save with the given recipe. + * @param userId userId with user. + * @param recipe recipe to save. + */ + public void saveRecipe(int userId, Recipe recipe) { + try { + recipeSearchInputBoundaryUseCase.saveRecipe(userId, recipe); + } + catch (Exception e) { + // Error will be handled by the presenter + } + } +} diff --git a/src/main/java/interface_adapter/recipe_search/RecipeSearchPresenter.java b/src/main/java/interface_adapter/recipe_search/RecipeSearchPresenter.java new file mode 100644 index 000000000..0bbaf69c2 --- /dev/null +++ b/src/main/java/interface_adapter/recipe_search/RecipeSearchPresenter.java @@ -0,0 +1,118 @@ +package interface_adapter.recipe_search; + +import entity.Recipe; +import entity.Ingredient; +import use_case.recipe_search.RecipeSearchOutputBoundary; + +import java.util.List; + +/** + * Presenter for the recipe search use case. + */ +public class RecipeSearchPresenter implements RecipeSearchOutputBoundary { + private final RecipeSearchViewModel viewModel; + + public RecipeSearchPresenter(RecipeSearchViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void presentRecipes(List recipes) { + RecipeSearchState state = new RecipeSearchState(); + state.setRecipes(recipes); + + // Format each recipe manually without streams + List recipeResults = new java.util.ArrayList<>(); + for (Recipe recipe : recipes) { + recipeResults.add(formatRecipe(recipe)); + } + + state.setRecipeResults(recipeResults); + viewModel.setState(state); + viewModel.firePropertyChanged(); + } + + /** + * Formats a recipe into an HTML string. + */ + private String formatRecipe(Recipe recipe) { + StringBuilder description = new StringBuilder(); + description.append(""); + description.append(formatTitleAndServings(recipe)).append("
"); + description.append(formatIngredients(recipe.getIngredients())).append("
"); + description.append(""); + return description.toString(); + } + + /** + * Formats the recipe title and servings information. + */ + private String formatTitleAndServings(Recipe recipe) { + return String.format("✦ %s
👥 Servings: %d", + recipe.getTitle().toUpperCase(), + recipe.getServings()); + } + + /** + * Formats the ingredients list as a single line. + */ + private String formatIngredients(List ingredients) { + StringBuilder ingredientsLine = new StringBuilder("📝 INGREDIENTS: "); + + // Iterate through ingredients and format each one + for (int i = 0; i < ingredients.size(); i++) { + Ingredient ingredient = ingredients.get(i); + ingredientsLine.append(formatIngredient(ingredient)); + + // Add a comma if not the last ingredient + if (i < ingredients.size() - 1) { + ingredientsLine.append(", "); + } + } + + return ingredientsLine.toString(); + } + + /** + * Formats an individual ingredient. + */ + private String formatIngredient(Ingredient ingredient) { + if (ingredient.getQuantity() == 0) { + return ingredient.getName(); + } + + String quantity = formatQuantity(ingredient.getQuantity()); + String unit = (ingredient.getUnit() == null || ingredient.getUnit().trim().isEmpty() || + ingredient.getUnit().equalsIgnoreCase("")) + ? "" + : ingredient.getUnit() + " "; + + return quantity + unit + ingredient.getName(); + } + + /** + * Formats the quantity of an ingredient to show up to 2 decimal places if necessary. + */ + private String formatQuantity(double quantity) { + return quantity == Math.floor(quantity) + ? String.valueOf((int) quantity) + : String.format("%.2f", quantity); + } + + @Override + public void presentError(String error) { + RecipeSearchState state = new RecipeSearchState(); + state.setError(error); + viewModel.setState(state); + viewModel.firePropertyChanged(); + } + + @Override + public void presentSaveSuccess(Recipe recipe) { + RecipeSearchState currentState = (RecipeSearchState) viewModel.getState(); + RecipeSearchState newState = new RecipeSearchState(currentState); + newState.setMessage(String.format("Recipe '%s' saved successfully!", recipe.getTitle())); + viewModel.setState(newState); + viewModel.firePropertyChanged(); + } +} diff --git a/src/main/java/interface_adapter/recipe_search/RecipeSearchState.java b/src/main/java/interface_adapter/recipe_search/RecipeSearchState.java new file mode 100644 index 000000000..692a69829 --- /dev/null +++ b/src/main/java/interface_adapter/recipe_search/RecipeSearchState.java @@ -0,0 +1,118 @@ +package interface_adapter.recipe_search; + +import entity.Recipe; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the state for recipe search functionality. + */ +public class RecipeSearchState { + private List ingredients = new ArrayList<>(); + private List recipeResults = new ArrayList<>(); + private List recipes = new ArrayList<>(); + private String error; + private String message; + + /** + * Copy constructor for RecipeSearchState. + * @param copy The state to copy from + */ + public RecipeSearchState(RecipeSearchState copy) { + if (copy != null) { + ingredients = new ArrayList<>(copy.ingredients); + recipeResults = new ArrayList<>(copy.recipeResults); + recipes = new ArrayList<>(copy.recipes); + error = copy.error; + message = copy.message; + } + } + + /** + * Default constructor for RecipeSearchState. + */ + public RecipeSearchState() { + + } + + /** + * Getter for ingredients. + * @return A copy of the ingredients list + */ + public List getIngredients() { + return new ArrayList<>(ingredients); + } + + /** + * Setter for ingredients. + * @param ingredients The ingredients to set + */ + public void setIngredients(List ingredients) { + this.ingredients = new ArrayList<>(ingredients); + } + + /** + * Getter for recipeResults. + * @return A copy of the recipeResults list + */ + public List getRecipeResults() { + return new ArrayList<>(recipeResults); + } + + /** + * Setter for recipeResults. + * @param recipeResults The recipeResults to set + */ + public void setRecipeResults(List recipeResults) { + this.recipeResults = new ArrayList<>(recipeResults); + } + + /** + * Getter for error. + * @return The error message + */ + public String getError() { + return error; + } + + /** + * Setter for error. + * @param error The error message to set + */ + public void setError(String error) { + this.error = error; + } + + /** + * Getter for message. + * @return The message + */ + public String getMessage() { + return message; + } + + /** + * Setter for message. + * @param message The message to set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Getter for recipes. + * @return A copy of the recipes list + */ + public List getRecipes() { + return new ArrayList<>(recipes); + } + + /** + * Setter for recipes. + * @param recipes The recipes to set + */ + public void setRecipes(List recipes) { + this.recipes = new ArrayList<>(recipes); + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/recipe_search/RecipeSearchViewModel.java b/src/main/java/interface_adapter/recipe_search/RecipeSearchViewModel.java new file mode 100644 index 000000000..d45787c45 --- /dev/null +++ b/src/main/java/interface_adapter/recipe_search/RecipeSearchViewModel.java @@ -0,0 +1,26 @@ +package interface_adapter.recipe_search; + +import interface_adapter.ViewModel; + +/** + * The ViewModel for the RecipeSearchView. + */ +public class RecipeSearchViewModel extends ViewModel { + // Attributes are created base on the components in RecipeSearchView class + public static final String TITLE_LABEL = "Search For Recipes"; + public static final String ADD_INGREDIENT_BUTTON_LABEL = "Add Ingredient"; + public static final String REMOVE_INGREDIENT_BUTTON_LABEL = "Remove Ingredient"; + public static final String SEARCH_BUTTON_LABEL = "Search"; + public static final String SAVE_RECIPE_LABEL = "Save Recipe"; + public static final String ANALYZE_NUTRITION_LABEL = "Analyze Nutrition"; + public static final String ENTER_INGREDIENT_LABEL = "Enter Ingredient:"; + public static final String INGREDIENT_TITLE_LABEL = "Ingredient:"; + public static final String RECIPE_RESULT_LABEL = "Recipe Result:"; + + public RecipeSearchViewModel() { + // RecipeSearchViewModel is currently a subclass of ViewModel base on the implementation + // of viewModel it must have a view name. + // However, this view name is not used in RecipeSearchView class. + super("Recipe Search"); + } +} diff --git a/src/main/java/interface_adapter/search_with_restriction/RestrictionController.java b/src/main/java/interface_adapter/search_with_restriction/RestrictionController.java new file mode 100644 index 000000000..501bf1872 --- /dev/null +++ b/src/main/java/interface_adapter/search_with_restriction/RestrictionController.java @@ -0,0 +1,30 @@ +package interface_adapter.search_with_restriction; + +import java.util.List; +import java.util.Map; + +import use_case.search_with_restriction.SearchWithRestrictionInputBoundary; + +/** + * Controller for the recipe search with restriction functionality. + */ +public class RestrictionController { + private final SearchWithRestrictionInputBoundary searchWithRestrictionInputBoundaryUseCase; + + public RestrictionController(SearchWithRestrictionInputBoundary searchWithRestrictionInputBoundary) { + this.searchWithRestrictionInputBoundaryUseCase = searchWithRestrictionInputBoundary; + } + + /** + * Execute a recipe search with the given restrictions. + * @param ingredients Map of ingredients to search for + */ + public void executeRestrictionSearch(Map> ingredients) { + try { + searchWithRestrictionInputBoundaryUseCase.searchRestrictionRecipes(ingredients); + } + catch (Exception exception) { + // Error will be handled by the presenter through output boundary + } + } +} diff --git a/src/main/java/interface_adapter/search_with_restriction/RestrictionPresenter.java b/src/main/java/interface_adapter/search_with_restriction/RestrictionPresenter.java new file mode 100644 index 000000000..c76417e92 --- /dev/null +++ b/src/main/java/interface_adapter/search_with_restriction/RestrictionPresenter.java @@ -0,0 +1,114 @@ +package interface_adapter.search_with_restriction; + +import java.util.List; + +import entity.Ingredient; +import entity.Recipe; +import use_case.search_with_restriction.SearchWithRestrictionOutputBoundary; + +/** + * Presenter for the recipe search with restriction use case. + */ +public class RestrictionPresenter implements SearchWithRestrictionOutputBoundary { + private final RestrictionViewModel restrictionViewModel; + + public RestrictionPresenter(RestrictionViewModel restrictionViewModel) { + this.restrictionViewModel = restrictionViewModel; + } + + @Override + public void presentRecipes(List recipes) { + final RestrictionState state = new RestrictionState(); + state.setRecipes(recipes); + + // Format each recipe manually without streams + final List recipeResults = new java.util.ArrayList<>(); + for (Recipe recipe : recipes) { + recipeResults.add(formatRecipe(recipe)); + } + + state.setRecipeResults(recipeResults); + restrictionViewModel.setState(state); + restrictionViewModel.firePropertyChanged(); + } + + /** + * Formats a recipe into an HTML string. + * @param recipe recipe that needs to be formated. + */ + private String formatRecipe(Recipe recipe) { + final StringBuilder description = new StringBuilder(); + description.append(""); + description.append(formatTitleAndServings(recipe)).append("
"); + description.append(formatIngredients(recipe.getIngredients())).append("
"); + description.append(""); + return description.toString(); + } + + /** + * Formats the recipe title and servings information. + * @param recipe recipe that needs to be formated. + */ + private String formatTitleAndServings(Recipe recipe) { + return String.format("✦ %s
👥 Servings: %d", + recipe.getTitle().toUpperCase(), + recipe.getServings()); + } + + /** + * Formats the ingredients list as a single line. + * @param ingredients ingredients that needs to be formated. + */ + private String formatIngredients(List ingredients) { + final StringBuilder ingredientsLine = new StringBuilder("📝 INGREDIENTS: "); + + // Iterate through ingredients and format each one + for (int i = 0; i < ingredients.size(); i++) { + final Ingredient ingredient = ingredients.get(i); + ingredientsLine.append(formatIngredient(ingredient)); + + // Add a comma if not the last ingredient + if (i < ingredients.size() - 1) { + ingredientsLine.append(", "); + } + } + + return ingredientsLine.toString(); + } + + /** + * Formats an individual ingredient. + * @param ingredient ingredient that needs to be formated. + */ + private String formatIngredient(Ingredient ingredient) { + if (ingredient.getQuantity() == 0) { + return ingredient.getName(); + } + + final String quantity = formatQuantity(ingredient.getQuantity()); + final String unit = (ingredient.getUnit() == null || ingredient.getUnit().trim().isEmpty() || + ingredient.getUnit().equalsIgnoreCase("")) + ? "" + : ingredient.getUnit() + " "; + + return quantity + unit + ingredient.getName(); + } + + /** + * Formats the quantity of an ingredient to show up to 2 decimal places if necessary. + * @param quantity quantity that needs to be formated. + */ + private String formatQuantity(double quantity) { + return quantity == Math.floor(quantity) + ? String.valueOf((int) quantity) + : String.format("%.2f", quantity); + } + + @Override + public void presentError(String error) { + final RestrictionState state = new RestrictionState(); + state.setError(error); + restrictionViewModel.setState(state); + restrictionViewModel.firePropertyChanged(); + } +} diff --git a/src/main/java/interface_adapter/search_with_restriction/RestrictionState.java b/src/main/java/interface_adapter/search_with_restriction/RestrictionState.java new file mode 100644 index 000000000..93d889371 --- /dev/null +++ b/src/main/java/interface_adapter/search_with_restriction/RestrictionState.java @@ -0,0 +1,104 @@ +package interface_adapter.search_with_restriction; + +import java.util.ArrayList; +import java.util.List; + +import entity.Recipe; + +/** + * State for Restriction search. + */ +public class RestrictionState { + private List ingredients = new ArrayList<>(); + private List recipeResults = new ArrayList<>(); + private List recipes = new ArrayList<>(); + private String error; + private String message; + + /** + * Default constructor for RestrictionState. + */ + public RestrictionState() { + // No specific actions required. + } + + /** + * Getter for ingredients. + * @return A copy of the ingredients list + */ + public List getIngredients() { + return new ArrayList<>(ingredients); + } + + /** + * Setter for ingredients. + * @param ingredients The ingredients to set + */ + public void setIngredients(List ingredients) { + this.ingredients = new ArrayList<>(ingredients); + } + + /** + * Getter for recipeResults. + * @return A copy of the recipeResults list + */ + public List getRecipeResults() { + return new ArrayList<>(recipeResults); + } + + /** + * Setter for recipeResults. + * @param recipeResults The recipeResults to set + */ + public void setRecipeResults(List recipeResults) { + this.recipeResults = new ArrayList<>(recipeResults); + } + + /** + * Getter for error. + * @return The error message + */ + public String getError() { + return error; + } + + /** + * Setter for error. + * @param error The error message to set + */ + public void setError(String error) { + this.error = error; + } + + /** + * Getter for message. + * @return The message + */ + public String getMessage() { + return message; + } + + /** + * Setter for message. + * @param message The message to set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Getter for recipes. + * @return A copy of the recipes list + */ + public List getRecipes() { + return new ArrayList<>(recipes); + } + + /** + * Setter for recipes. + * @param recipes The recipes to set + */ + public void setRecipes(List recipes) { + this.recipes = new ArrayList<>(recipes); + } +} diff --git a/src/main/java/interface_adapter/search_with_restriction/RestrictionViewModel.java b/src/main/java/interface_adapter/search_with_restriction/RestrictionViewModel.java new file mode 100644 index 000000000..02bd27b32 --- /dev/null +++ b/src/main/java/interface_adapter/search_with_restriction/RestrictionViewModel.java @@ -0,0 +1,20 @@ +package interface_adapter.search_with_restriction; + +import interface_adapter.ViewModel; + +/** + * The Restriction ViewModel for the RecipeSearchView. + */ +public class RestrictionViewModel extends ViewModel { + // Attributes are created base on the components in RecipeSearchView class + public static final String ADD_RESTRICTION_LABEL = "Add Restriction"; + public static final String REMOVE_RESTRICTION_LABEL = "Remove Restriction"; + public static final String RESTRICTION_TITLE_LABEL = "Restriction:"; + + public RestrictionViewModel() { + // RestrictionViewModel is currently a subclass of ViewModel base on the implementation + // of viewModel it must have a view name. + // However, this view name is not used in RecipeSearchView class. + super("Recipe Search"); + } +} diff --git a/src/main/java/interface_adapter/serving_adjust/ServingAdjustController.java b/src/main/java/interface_adapter/serving_adjust/ServingAdjustController.java new file mode 100644 index 000000000..194a378e9 --- /dev/null +++ b/src/main/java/interface_adapter/serving_adjust/ServingAdjustController.java @@ -0,0 +1,31 @@ +package interface_adapter.serving_adjust; + +import java.util.List; + +import entity.Recipe; +import use_case.serving_adjust.ServingAdjustInputBoundary; + +/** + * Controller class for managing serving adjustments for recipes. + */ +public class ServingAdjustController { + + private final ServingAdjustInputBoundary servingAdjust; + + /** + * Constructs with the specified serving adjustment use case. + * @param servingAdjust The input boundary for serving adjustment use case + */ + public ServingAdjustController(ServingAdjustInputBoundary servingAdjust) { + this.servingAdjust = servingAdjust; + } + + /** + * Updates the servings for a list of recipes by delegating the operation to the serving adjustment use case. + * @param newServings The new number of servings to apply + * @param recipes The list of recipes to update + */ + public void updateServingsForAll(int newServings, List recipes) { + servingAdjust.adjustServings(newServings, recipes); + } +} diff --git a/src/main/java/interface_adapter/serving_adjust/ServingAdjustPresenter.java b/src/main/java/interface_adapter/serving_adjust/ServingAdjustPresenter.java new file mode 100644 index 000000000..54fcb04fa --- /dev/null +++ b/src/main/java/interface_adapter/serving_adjust/ServingAdjustPresenter.java @@ -0,0 +1,41 @@ +package interface_adapter.serving_adjust; + +import java.util.List; + +import entity.Recipe; +import use_case.serving_adjust.ServingAdjustOutputBoundary; + +/** + * Presenter class for serving adjustment functionality. + */ +public class ServingAdjustPresenter implements ServingAdjustOutputBoundary { + + private final ServingAdjustViewModel viewModel; + + /** + * Constructs ServingAdjustPresenter with the specified view model. + * + * @param viewModel The view model to update with the serving adjustment data + */ + public ServingAdjustPresenter(ServingAdjustViewModel viewModel) { + this.viewModel = viewModel; + } + + /** + * Updates the view model with the list of recipes that have been adjusted for servings. + * + * @param updatedRecipes The list of recipes with adjusted servings + */ + @Override + public void presentUpdatedRecipes(List updatedRecipes) { + try { + viewModel.updateRecipes(updatedRecipes); + } + catch (IllegalArgumentException exIllegalArgument) { + viewModel.setError("Invalid argument while updating recipes: " + exIllegalArgument.getMessage()); + } + catch (IllegalStateException exIllegalState) { + viewModel.setError("Illegal state encountered while updating recipes: " + exIllegalState.getMessage()); + } + } +} diff --git a/src/main/java/interface_adapter/serving_adjust/ServingAdjustState.java b/src/main/java/interface_adapter/serving_adjust/ServingAdjustState.java new file mode 100644 index 000000000..242b11da1 --- /dev/null +++ b/src/main/java/interface_adapter/serving_adjust/ServingAdjustState.java @@ -0,0 +1,128 @@ +package interface_adapter.serving_adjust; + +import java.util.ArrayList; +import java.util.List; + +import entity.Recipe; + +/** + * Represents the state for serving adjustment functionality. + */ +public class ServingAdjustState { + private int currentServings = 1; + private List recipes = new ArrayList<>(); + private String error; + private String message; + + /** + * Copy constructor for ServingAdjustState. + * + * @param copy The state to copy from. Must not be null. + * @throws NullPointerException if the provided state is null. + */ + public ServingAdjustState(ServingAdjustState copy) { + if (copy == null) { + throw new NullPointerException("Copy source cannot be null."); + } + this.currentServings = copy.currentServings; + this.recipes = new ArrayList<>(copy.recipes); + this.error = copy.error; + this.message = copy.message; + } + + /** + * Default constructor for ServingAdjustState. + * Initializes the state with default values. + */ + public ServingAdjustState() { + } + + /** + * Gets the current servings. + * + * @return The current servings value. Defaults to 1. + */ + public int getCurrentServings() { + return currentServings; + } + + /** + * Sets the current servings. + * Ensures the servings value is greater than zero. + * + * @param currentServings The number of servings to set. + * @throws IllegalArgumentException if the servings value is less than or equal to zero. + */ + public void setCurrentServings(int currentServings) { + if (currentServings > 0) { + this.currentServings = currentServings; + } + else { + throw new IllegalArgumentException("Servings must be greater than zero."); + } + } + + /** + * Gets the list of recipes. + * + * @return A copy of the current list of recipes. + */ + public List getRecipes() { + return new ArrayList<>(recipes); + } + + /** + * Sets the list of recipes. + * Replaces the current list of recipes with a new list. + * + * @param recipes The new list of recipes to set. + * Must not be null. + * @throws NullPointerException if the provided list is null. + */ + public void setRecipes(List recipes) { + if (recipes == null) { + throw new NullPointerException("Recipes list cannot be null."); + } + this.recipes = new ArrayList<>(recipes); + } + + /** + * Gets the current error message. + * + * @return The error message, or null if no error is present. + */ + public String getError() { + return error; + } + + /** + * Sets the error message. + * Clears any existing message when an error is set. + * + * @param error The error message to set. + */ + public void setError(String error) { + this.error = error; + this.message = null; + } + + /** + * Gets the current informational message. + * + * @return The informational message, or null if no message is present. + */ + public String getMessage() { + return message; + } + + /** + * Sets the informational message. + * Clears any existing error when a message is set. + * + * @param message The informational message to set. + */ + public void setMessage(String message) { + this.message = message; + this.error = null; + } +} diff --git a/src/main/java/interface_adapter/serving_adjust/ServingAdjustViewModel.java b/src/main/java/interface_adapter/serving_adjust/ServingAdjustViewModel.java new file mode 100644 index 000000000..42782ff87 --- /dev/null +++ b/src/main/java/interface_adapter/serving_adjust/ServingAdjustViewModel.java @@ -0,0 +1,84 @@ +package interface_adapter.serving_adjust; + +import java.util.List; + +import entity.Recipe; +import interface_adapter.ViewModel; + +/** + * ViewModel for serving adjustment functionality. + */ +public class ServingAdjustViewModel extends ViewModel { + + /** + * Property name for updated recipes notifications. + */ + public static final String UPDATED_RECIPES_PROPERTY = "updatedRecipes"; + + /** + * Property name for updated servings notifications. + */ + public static final String UPDATED_SERVINGS_PROPERTY = "updatedServings"; + + /** + * Initializes the ServingAdjustViewModel with a default state. + */ + public ServingAdjustViewModel() { + super("Serving Adjustment"); + setState(new ServingAdjustState()); + } + + /** + * Updates the list of recipes in the state and notifies listeners. + * + * @param recipes The list of updated recipes. Must not be null. + * @throws NullPointerException if the provided recipes list is null. + */ + public void updateRecipes(List recipes) { + if (recipes == null) { + throw new NullPointerException("Recipes list cannot be null."); + } + + final ServingAdjustState currentState = getState(); + if (currentState != null) { + currentState.setRecipes(recipes); + firePropertyChanged(UPDATED_RECIPES_PROPERTY); + } + } + + /** + * Updates the servings count in the state and notifies listeners. + * + * @param servings The updated servings count. Must be greater than zero. + * @throws IllegalArgumentException if servings is less than or equal to zero. + */ + public void updateServings(int servings) { + if (servings <= 0) { + throw new IllegalArgumentException("Servings must be greater than zero."); + } + + final ServingAdjustState currentState = getState(); + if (currentState != null) { + currentState.setCurrentServings(servings); + firePropertyChanged(UPDATED_SERVINGS_PROPERTY); + } + } + + /** + * Sets an error message in the state and notifies listeners. + * + * @param error The error message to set. Must not be null or empty. + * @throws IllegalArgumentException if the error message is null or empty. + */ + public void setError(String error) { + if (error == null || error.trim().isEmpty()) { + throw new IllegalArgumentException("Error message cannot be null or empty."); + } + + final ServingAdjustState currentState = getState(); + if (currentState != null) { + currentState.setError(error); + firePropertyChanged("error"); + } + } +} diff --git a/src/main/java/interface_adapter/start/FrontPageController.java b/src/main/java/interface_adapter/start/FrontPageController.java new file mode 100644 index 000000000..bee788173 --- /dev/null +++ b/src/main/java/interface_adapter/start/FrontPageController.java @@ -0,0 +1,50 @@ +package interface_adapter.start; + +import view.FrontPageView; +import view.MealPlanningView; +import view.RecipeSearchView; + +import javax.swing.*; + +public class FrontPageController { + + public static final int HEIGHT = 600; + public static final int WIDTH = 800; + + private FrontPageView view; + + public FrontPageController(FrontPageView view) { + this.view = view; + } + + public static void goToRecipeSearch(RecipeSearchView recipeSearchView, MealPlanningView mealPlanningView) { + final JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + frame.setTitle("Recipe Wiz"); + frame.setSize(WIDTH, HEIGHT); + + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.addTab("Recipe Search", recipeSearchView); + tabbedPane.addTab("Meal Planning", mealPlanningView); + + frame.add(tabbedPane); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void goToMealPlanning(RecipeSearchView recipeSearchView, MealPlanningView mealPlanningView) { + final JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + frame.setTitle("Recipe Wiz"); + frame.setSize(WIDTH, HEIGHT); + + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.addTab("Recipe Search", recipeSearchView); + tabbedPane.addTab("Meal Planning", mealPlanningView); + tabbedPane.setSelectedIndex(1); + + frame.add(tabbedPane); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } +} diff --git a/src/main/java/resources/frontpageview.png b/src/main/java/resources/frontpageview.png new file mode 100644 index 000000000..0b0f4439e Binary files /dev/null and b/src/main/java/resources/frontpageview.png differ diff --git a/src/main/java/resources/img_1.png b/src/main/java/resources/img_1.png new file mode 100644 index 000000000..dcf4027d5 Binary files /dev/null and b/src/main/java/resources/img_1.png differ diff --git a/src/main/java/resources/readme1.png b/src/main/java/resources/readme1.png new file mode 100644 index 000000000..c5ea8709c Binary files /dev/null and b/src/main/java/resources/readme1.png differ diff --git a/src/main/java/resources/readme2.png b/src/main/java/resources/readme2.png new file mode 100644 index 000000000..a0a6adc5d Binary files /dev/null and b/src/main/java/resources/readme2.png differ diff --git a/src/main/java/resources/readme3.png b/src/main/java/resources/readme3.png new file mode 100644 index 000000000..34d35dc9b Binary files /dev/null and b/src/main/java/resources/readme3.png differ diff --git a/src/main/java/resources/readme4.png b/src/main/java/resources/readme4.png new file mode 100644 index 000000000..74421f46a Binary files /dev/null and b/src/main/java/resources/readme4.png differ diff --git a/src/main/java/resources/readme5.png b/src/main/java/resources/readme5.png new file mode 100644 index 000000000..0be678c61 Binary files /dev/null and b/src/main/java/resources/readme5.png differ diff --git a/src/main/java/resources/readme6.png b/src/main/java/resources/readme6.png new file mode 100644 index 000000000..ea303d1c3 Binary files /dev/null and b/src/main/java/resources/readme6.png differ diff --git a/src/main/java/use_case/change_password/ChangePasswordInputBoundary.java b/src/main/java/use_case/change_password/ChangePasswordInputBoundary.java new file mode 100644 index 000000000..06ae9448e --- /dev/null +++ b/src/main/java/use_case/change_password/ChangePasswordInputBoundary.java @@ -0,0 +1,14 @@ +package use_case.change_password; + +/** + * The Change Password Use Case. + */ +public interface ChangePasswordInputBoundary { + + /** + * Execute the Change Password Use Case. + * @param changePasswordInputData the input data for this use case + */ + void execute(ChangePasswordInputData changePasswordInputData); + +} diff --git a/src/main/java/use_case/change_password/ChangePasswordInputData.java b/src/main/java/use_case/change_password/ChangePasswordInputData.java new file mode 100644 index 000000000..8e09d8d12 --- /dev/null +++ b/src/main/java/use_case/change_password/ChangePasswordInputData.java @@ -0,0 +1,24 @@ +package use_case.change_password; + +/** + * The input data for the Change Password Use Case. + */ +public class ChangePasswordInputData { + + private final String password; + private final String username; + + public ChangePasswordInputData(String password, String username) { + this.password = password; + this.username = username; + } + + String getPassword() { + return password; + } + + String getUsername() { + return username; + } + +} diff --git a/src/main/java/use_case/change_password/ChangePasswordInteractor.java b/src/main/java/use_case/change_password/ChangePasswordInteractor.java new file mode 100644 index 000000000..df91196c1 --- /dev/null +++ b/src/main/java/use_case/change_password/ChangePasswordInteractor.java @@ -0,0 +1,32 @@ +package use_case.change_password; + +import entity.User; +import entity.UserFactory; + +/** + * The Change Password Interactor. + */ +public class ChangePasswordInteractor implements ChangePasswordInputBoundary { + private final ChangePasswordUserDataAccessInterface userDataAccessObject; + private final ChangePasswordOutputBoundary userPresenter; + private final UserFactory userFactory; + + public ChangePasswordInteractor(ChangePasswordUserDataAccessInterface changePasswordDataAccessInterface, + ChangePasswordOutputBoundary changePasswordOutputBoundary, + UserFactory userFactory) { + this.userDataAccessObject = changePasswordDataAccessInterface; + this.userPresenter = changePasswordOutputBoundary; + this.userFactory = userFactory; + } + + @Override + public void execute(ChangePasswordInputData changePasswordInputData) { + final User user = userFactory.create(changePasswordInputData.getUsername(), + changePasswordInputData.getPassword()); + userDataAccessObject.changePassword(user); + + final ChangePasswordOutputData changePasswordOutputData = new ChangePasswordOutputData(user.getName(), + false); + userPresenter.prepareSuccessView(changePasswordOutputData); + } +} diff --git a/src/main/java/use_case/change_password/ChangePasswordOutputBoundary.java b/src/main/java/use_case/change_password/ChangePasswordOutputBoundary.java new file mode 100644 index 000000000..fce28367b --- /dev/null +++ b/src/main/java/use_case/change_password/ChangePasswordOutputBoundary.java @@ -0,0 +1,18 @@ +package use_case.change_password; + +/** + * The output boundary for the Change Password Use Case. + */ +public interface ChangePasswordOutputBoundary { + /** + * Prepares the success view for the Change Password Use Case. + * @param outputData the output data + */ + void prepareSuccessView(ChangePasswordOutputData outputData); + + /** + * Prepares the failure view for the Change Password Use Case. + * @param errorMessage the explanation of the failure + */ + void prepareFailView(String errorMessage); +} diff --git a/src/main/java/use_case/change_password/ChangePasswordOutputData.java b/src/main/java/use_case/change_password/ChangePasswordOutputData.java new file mode 100644 index 000000000..b47b83fbc --- /dev/null +++ b/src/main/java/use_case/change_password/ChangePasswordOutputData.java @@ -0,0 +1,24 @@ +package use_case.change_password; + +/** + * Output Data for the Change Password Use Case. + */ +public class ChangePasswordOutputData { + + private final String username; + + private final boolean useCaseFailed; + + public ChangePasswordOutputData(String username, boolean useCaseFailed) { + this.username = username; + this.useCaseFailed = useCaseFailed; + } + + public String getUsername() { + return username; + } + + public boolean isUseCaseFailed() { + return useCaseFailed; + } +} diff --git a/src/main/java/use_case/change_password/ChangePasswordUserDataAccessInterface.java b/src/main/java/use_case/change_password/ChangePasswordUserDataAccessInterface.java new file mode 100644 index 000000000..6b73ab6b0 --- /dev/null +++ b/src/main/java/use_case/change_password/ChangePasswordUserDataAccessInterface.java @@ -0,0 +1,15 @@ +package use_case.change_password; + +import entity.User; + +/** + * The interface of the DAO for the Change Password Use Case. + */ +public interface ChangePasswordUserDataAccessInterface { + + /** + * Updates the system to record this user's password. + * @param user the user whose password is to be updated + */ + void changePassword(User user); +} diff --git a/src/main/java/use_case/login/LoginInputBoundary.java b/src/main/java/use_case/login/LoginInputBoundary.java new file mode 100644 index 000000000..faf72dc96 --- /dev/null +++ b/src/main/java/use_case/login/LoginInputBoundary.java @@ -0,0 +1,13 @@ +package use_case.login; + +/** + * Input Boundary for actions which are related to logging in. + */ +public interface LoginInputBoundary { + + /** + * Executes the login use case. + * @param loginInputData the input data + */ + void execute(LoginInputData loginInputData); +} diff --git a/src/main/java/use_case/login/LoginInputData.java b/src/main/java/use_case/login/LoginInputData.java new file mode 100644 index 000000000..363316832 --- /dev/null +++ b/src/main/java/use_case/login/LoginInputData.java @@ -0,0 +1,24 @@ +package use_case.login; + +/** + * The Input Data for the Login Use Case. + */ +public class LoginInputData { + + private final String username; + private final String password; + + public LoginInputData(String username, String password) { + this.username = username; + this.password = password; + } + + String getUsername() { + return username; + } + + String getPassword() { + return password; + } + +} diff --git a/src/main/java/use_case/login/LoginInteractor.java b/src/main/java/use_case/login/LoginInteractor.java new file mode 100644 index 000000000..5b36ddcd8 --- /dev/null +++ b/src/main/java/use_case/login/LoginInteractor.java @@ -0,0 +1,40 @@ +package use_case.login; + +import entity.User; + +/** + * The Login Interactor. + */ +public class LoginInteractor implements LoginInputBoundary { + private final LoginUserDataAccessInterface userDataAccessObject; + private final LoginOutputBoundary loginPresenter; + + public LoginInteractor(LoginUserDataAccessInterface userDataAccessInterface, + LoginOutputBoundary loginOutputBoundary) { + this.userDataAccessObject = userDataAccessInterface; + this.loginPresenter = loginOutputBoundary; + } + + @Override + public void execute(LoginInputData loginInputData) { + final String username = loginInputData.getUsername(); + final String password = loginInputData.getPassword(); + if (!userDataAccessObject.existsByName(username)) { + loginPresenter.prepareFailView(username + ": Account does not exist."); + } + else { + final String pwd = userDataAccessObject.get(username).getPassword(); + if (!password.equals(pwd)) { + loginPresenter.prepareFailView("Incorrect password for \"" + username + "\"."); + } + else { + + final User user = userDataAccessObject.get(loginInputData.getUsername()); + + userDataAccessObject.setCurrentUsername(user.getName()); + final LoginOutputData loginOutputData = new LoginOutputData(user.getName(), false); + loginPresenter.prepareSuccessView(loginOutputData); + } + } + } +} diff --git a/src/main/java/use_case/login/LoginOutputBoundary.java b/src/main/java/use_case/login/LoginOutputBoundary.java new file mode 100644 index 000000000..08bc4731f --- /dev/null +++ b/src/main/java/use_case/login/LoginOutputBoundary.java @@ -0,0 +1,18 @@ +package use_case.login; + +/** + * The output boundary for the Login Use Case. + */ +public interface LoginOutputBoundary { + /** + * Prepares the success view for the Login Use Case. + * @param outputData the output data + */ + void prepareSuccessView(LoginOutputData outputData); + + /** + * Prepares the failure view for the Login Use Case. + * @param errorMessage the explanation of the failure + */ + void prepareFailView(String errorMessage); +} diff --git a/src/main/java/use_case/login/LoginOutputData.java b/src/main/java/use_case/login/LoginOutputData.java new file mode 100644 index 000000000..3ea119a8f --- /dev/null +++ b/src/main/java/use_case/login/LoginOutputData.java @@ -0,0 +1,20 @@ +package use_case.login; + +/** + * Output Data for the Login Use Case. + */ +public class LoginOutputData { + + private final String username; + private final boolean useCaseFailed; + + public LoginOutputData(String username, boolean useCaseFailed) { + this.username = username; + this.useCaseFailed = useCaseFailed; + } + + public String getUsername() { + return username; + } + +} diff --git a/src/main/java/use_case/login/LoginUserDataAccessInterface.java b/src/main/java/use_case/login/LoginUserDataAccessInterface.java new file mode 100644 index 000000000..681e8a52e --- /dev/null +++ b/src/main/java/use_case/login/LoginUserDataAccessInterface.java @@ -0,0 +1,41 @@ +package use_case.login; + +import entity.User; + +/** + * DAO for the Login Use Case. + */ +public interface LoginUserDataAccessInterface { + + /** + * Checks if the given username exists. + * @param username the username to look for + * @return true if a user with the given username exists; false otherwise + */ + boolean existsByName(String username); + + /** + * Saves the user. + * @param user the user to save + */ + void save(User user); + + /** + * Returns the user with the given username. + * @param username the username to look up + * @return the user with the given username + */ + User get(String username); + + /** + * Returns the username of the curren user of the application. + * @return the username of the current user; null indicates that no one is logged into the application. + */ + String getCurrentUsername(); + + /** + * Sets the username indicating who is the current user of the application. + * @param username the new current username; null to indicate that no one is currently logged into the application. + */ + void setCurrentUsername(String username); +} diff --git a/src/main/java/use_case/logout/LogoutInputBoundary.java b/src/main/java/use_case/logout/LogoutInputBoundary.java new file mode 100644 index 000000000..189f50168 --- /dev/null +++ b/src/main/java/use_case/logout/LogoutInputBoundary.java @@ -0,0 +1,13 @@ +package use_case.logout; + +/** + * Input Boundary for actions which are related to logging in. + */ +public interface LogoutInputBoundary { + + /** + * Executes the Logout use case. + * @param LogoutInputData the input data + */ + void execute(LogoutInputData LogoutInputData); +} diff --git a/src/main/java/use_case/logout/LogoutInputData.java b/src/main/java/use_case/logout/LogoutInputData.java new file mode 100644 index 000000000..d0df964fe --- /dev/null +++ b/src/main/java/use_case/logout/LogoutInputData.java @@ -0,0 +1,17 @@ +package use_case.logout; + +/** + * The Input Data for the Logout Use Case. + */ +public class LogoutInputData { + private final String username; + + public LogoutInputData(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + +} diff --git a/src/main/java/use_case/logout/LogoutInteractor.java b/src/main/java/use_case/logout/LogoutInteractor.java new file mode 100644 index 000000000..d0bd6a7b1 --- /dev/null +++ b/src/main/java/use_case/logout/LogoutInteractor.java @@ -0,0 +1,23 @@ +package use_case.logout; + +/** + * The Logout Interactor. + */ +public class LogoutInteractor implements LogoutInputBoundary { + private LogoutUserDataAccessInterface userDataAccessObject; + private LogoutOutputBoundary logoutPresenter; + + public LogoutInteractor(LogoutUserDataAccessInterface userDataAccessInterface, + LogoutOutputBoundary logoutOutputBoundary) { + this.userDataAccessObject = userDataAccessInterface; + this.logoutPresenter = logoutOutputBoundary; + } + + @Override + public void execute(LogoutInputData logoutInputData) { + final String username = logoutInputData.getUsername(); + userDataAccessObject.setCurrentUsername(null); + final LogoutOutputData logoutOutputData = new LogoutOutputData(username, false); + logoutPresenter.prepareSuccessView(logoutOutputData); + } +} diff --git a/src/main/java/use_case/logout/LogoutOutputBoundary.java b/src/main/java/use_case/logout/LogoutOutputBoundary.java new file mode 100644 index 000000000..935a06bdc --- /dev/null +++ b/src/main/java/use_case/logout/LogoutOutputBoundary.java @@ -0,0 +1,18 @@ +package use_case.logout; + +/** + * The output boundary for the Login Use Case. + */ +public interface LogoutOutputBoundary { + /** + * Prepares the success view for the Login Use Case. + * @param outputData the output data + */ + void prepareSuccessView(LogoutOutputData outputData); + + /** + * Prepares the failure view for the Login Use Case. + * @param errorMessage the explanation of the failure + */ + void prepareFailView(String errorMessage); +} diff --git a/src/main/java/use_case/logout/LogoutOutputData.java b/src/main/java/use_case/logout/LogoutOutputData.java new file mode 100644 index 000000000..2bbd76532 --- /dev/null +++ b/src/main/java/use_case/logout/LogoutOutputData.java @@ -0,0 +1,23 @@ +package use_case.logout; + +/** + * Output Data for the Logout Use Case. + */ +public class LogoutOutputData { + + private final String username; + private final boolean useCaseFailed; + + public LogoutOutputData(String username, boolean useCaseFailed) { + this.username = username; + this.useCaseFailed = useCaseFailed; + } + + public String getUsername() { + return username; + } + + public boolean isUseCaseFailed() { + return useCaseFailed; + } +} diff --git a/src/main/java/use_case/logout/LogoutUserDataAccessInterface.java b/src/main/java/use_case/logout/LogoutUserDataAccessInterface.java new file mode 100644 index 000000000..8263700e2 --- /dev/null +++ b/src/main/java/use_case/logout/LogoutUserDataAccessInterface.java @@ -0,0 +1,19 @@ +package use_case.logout; + +/** + * DAO for the Logout Use Case. + */ +public interface LogoutUserDataAccessInterface { + + /** + * Returns the username of the curren user of the application. + * @return the username of the current user + */ + String getCurrentUsername(); + + /** + * Sets the username indicating who is the current user of the application. + * @param username the new current username + */ + void setCurrentUsername(String username); +} diff --git a/src/main/java/use_case/meal_planning/MealPlanningDataAccessInterface.java b/src/main/java/use_case/meal_planning/MealPlanningDataAccessInterface.java new file mode 100644 index 000000000..221869cbd --- /dev/null +++ b/src/main/java/use_case/meal_planning/MealPlanningDataAccessInterface.java @@ -0,0 +1,58 @@ +package use_case.meal_planning; + +import java.time.LocalDate; +import java.util.List; + +import entity.MealPlanEntry; + +/** + * Interface for managing meal planning data access operations. + * Provides methods to create, retrieve, update, and delete meal plan entries. + */ +public interface MealPlanningDataAccessInterface { + + /** + * Adds a new meal plan entry for a specific user. + * + * @param userId the unique identifier of the user + * @param recipeId the unique identifier of the recipe + * @param date the date for which the meal is planned + * @param mealType the type of meal (e.g., breakfast, lunch, dinner) + */ + void addMealPlanEntry(int userId, int recipeId, LocalDate date, String mealType); + + /** + * Removes a meal plan entry for a specific user. + * + * @param userId the unique identifier of the user + * @param mealPlanEntryId the unique identifier of the meal plan entry to remove + */ + void removeMealPlanEntry(int userId, int mealPlanEntryId); + + /** + * Updates the status of a meal plan entry. + * + * @param userId the unique identifier of the user + * @param entryId the unique identifier of the meal plan entry + * @param status the new status to set for the meal plan entry + */ + void updateMealStatus(int userId, int entryId, String status); + + /** + * Retrieves a specific meal plan entry. + * + * @param userId the unique identifier of the user + * @param entryId the unique identifier of the meal plan entry + * @return the requested meal plan entry + */ + MealPlanEntry getMealPlanEntry(int userId, int entryId); + + /** + * Retrieves all meal plan entries for a specific week. + * + * @param userId the unique identifier of the user + * @param weekStart the start date of the week + * @return a list of meal plan entries for the specified week + */ + List getWeeklyPlan(int userId, LocalDate weekStart); +} diff --git a/src/main/java/use_case/meal_planning/MealPlanningException.java b/src/main/java/use_case/meal_planning/MealPlanningException.java new file mode 100644 index 000000000..f95757361 --- /dev/null +++ b/src/main/java/use_case/meal_planning/MealPlanningException.java @@ -0,0 +1,51 @@ +package use_case.meal_planning; + +/** + * Custom exception class for meal planning related errors. + * Provides specific exception handling for meal planning operations. + */ +public class MealPlanningException extends RuntimeException { + + private final String errorCode; + + /** + * Constructs a new MealPlanningException with a message and error code. + * + * @param message detailed error message + * @param errorCode specific error code for the exception + */ + public MealPlanningException(String message, String errorCode) { + super(message); + this.errorCode = errorCode; + } + + /** + * Constructs a new MealPlanningException with a message, error code, and cause. + * + * @param message detailed error message + * @param errorCode specific error code for the exception + * @param cause the cause of this exception + */ + public MealPlanningException(String message, String errorCode, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + /** + * Gets the error code associated with this exception. + * + * @return the error code + */ + public String getErrorCode() { + return errorCode; + } + + // Common error codes for meal planning exceptions + public static final String INVALID_USER = "MP001"; + public static final String INVALID_RECIPE = "MP002"; + public static final String INVALID_DATE = "MP003"; + public static final String INVALID_MEAL_TYPE = "MP004"; + public static final String INVALID_ENTRY = "MP005"; + public static final String DATA_ACCESS_ERROR = "MP006"; + public static final String INITIALIZATION_ERROR = "MP007"; +} diff --git a/src/main/java/use_case/meal_planning/MealPlanningInputBoundary.java b/src/main/java/use_case/meal_planning/MealPlanningInputBoundary.java new file mode 100644 index 000000000..b90b0db2f --- /dev/null +++ b/src/main/java/use_case/meal_planning/MealPlanningInputBoundary.java @@ -0,0 +1,63 @@ +package use_case.meal_planning; + +import java.time.LocalDate; +import java.util.List; + +import entity.Recipe; + +/** + * Interface defining the core meal planning use case operations. + * Provides methods for managing meal plans, recipes, and calendar operations. + */ +public interface MealPlanningInputBoundary { + + /** + * Adds a meal for the calendar for a specific user. + * + * @param userId the ID of the user + * @param recipeId the ID of the recipe to add + * @param date the date to add the meal + * @param mealType the type of meal (breakfast, lunch, dinner) + */ + void addToCalendar(int userId, int recipeId, LocalDate date, String mealType); + + /** + * Removes a meal plan entry from the calendar. + * + * @param userId the ID of the user + * @param mealPlanEntryId the ID of the meal plan entry to remove + */ + void removeFromCalendar(int userId, int mealPlanEntryId); + + /** + * Retrieves meal plan entries for a specific week. + * + * @param userId the ID of the user + * @param weekStart the start date of the week + */ + void getCalendarWeek(int userId, LocalDate weekStart); + + /** + * Retrieves the list of saved recipes for a user. + * + * @param userId the ID of the user + * @return list of saved recipes + */ + List getSavedRecipes(int userId); + + /** + * Updates the status of a meal plan entry. + * + * @param userId the ID of the user + * @param entryId the ID of the meal plan entry + * @param status the new status to set + */ + void updateMealStatus(int userId, int entryId, String status); + + /** + * Initializes meal planning for a new user. + * + * @param userId the ID of the user + */ + void initializeMealPlanning(int userId); +} diff --git a/src/main/java/use_case/meal_planning/MealPlanningInteractor.java b/src/main/java/use_case/meal_planning/MealPlanningInteractor.java new file mode 100644 index 000000000..c2ba8c7f0 --- /dev/null +++ b/src/main/java/use_case/meal_planning/MealPlanningInteractor.java @@ -0,0 +1,150 @@ +package use_case.meal_planning; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; + +import data_access.SavedRecipesDataAccessInterface; +import entity.MealPlanEntry; +import entity.Recipe; + +/** + * Implements the MealPlanningInputBoundary interface to handle meal planning business logic. + * Coordinates between data access layer and output boundary for meal planning operations. + */ +public class MealPlanningInteractor implements MealPlanningInputBoundary { + private final MealPlanningDataAccessInterface dataAccessInterface; + private final SavedRecipesDataAccessInterface savedRecipesDataAccessInterface; + private final MealPlanningOutputBoundary outputBoundary; + + public MealPlanningInteractor(MealPlanningDataAccessInterface dataAccessInterface, + SavedRecipesDataAccessInterface savedRecipesDataAccessInterface, + MealPlanningOutputBoundary outputBoundary) { + this.dataAccessInterface = dataAccessInterface; + this.savedRecipesDataAccessInterface = savedRecipesDataAccessInterface; + this.outputBoundary = outputBoundary; + } + + @Override + public List getSavedRecipes(int userId) { + List recipes = List.of(); + try { + validateUserId(userId); + recipes = savedRecipesDataAccessInterface.getSavedRecipes(userId); + outputBoundary.presentSavedRecipes(recipes); + } + catch (MealPlanningException e) { + outputBoundary.presentError(e.getMessage()); + } + return recipes; + } + + @Override + public void updateMealStatus(int userId, int entryId, String status) { + try { + validateUserId(userId); + validateEntryId(entryId); + validateStatus(status); + + dataAccessInterface.updateMealStatus(userId, entryId, status); + outputBoundary.presentStatusUpdateSuccess("Meal status updated"); + } + catch (MealPlanningException e) { + outputBoundary.presentError(e.getMessage()); + } + } + + @Override + public void addToCalendar(int userId, int recipeId, LocalDate date, String mealType) { + try { + validateUserId(userId); + validateRecipeId(recipeId); + validateDate(date); + validateMealType(mealType); + + dataAccessInterface.addMealPlanEntry(userId, recipeId, date, mealType); + outputBoundary.presentAddSuccess("Recipe added to calendar"); + } + catch (MealPlanningException e) { + outputBoundary.presentError(e.getMessage()); + } + } + + @Override + public void removeFromCalendar(int userId, int mealPlanEntryId) { + try { + validateUserId(userId); + validateEntryId(mealPlanEntryId); + + dataAccessInterface.removeMealPlanEntry(userId, mealPlanEntryId); + outputBoundary.presentRemoveSuccess("Recipe removed from calendar"); + } + catch (MealPlanningException e) { + outputBoundary.presentError(e.getMessage()); + } + } + + @Override + public void getCalendarWeek(int userId, LocalDate weekStart) { + try { + validateUserId(userId); + validateDate(weekStart); + + List entries = dataAccessInterface.getWeeklyPlan(userId, weekStart); + outputBoundary.presentCalendarWeek(entries); + } + catch (MealPlanningException e) { + outputBoundary.presentError(e.getMessage()); + } + } + + @Override + public void initializeMealPlanning(int userId) { + try { + validateUserId(userId); + LocalDate currentWeekStart = LocalDate.now().with(DayOfWeek.MONDAY); + List entries = dataAccessInterface.getWeeklyPlan(userId, currentWeekStart); + outputBoundary.presentCalendarWeek(entries); + } + catch (MealPlanningException e) { + outputBoundary.presentError(e.getMessage()); + } + } + + // Validation methods + private void validateUserId(int userId) { + if (userId <= 0) { + throw new MealPlanningException("Invalid user ID", MealPlanningException.INVALID_USER); + } + } + + private void validateRecipeId(int recipeId) { + if (recipeId <= 0) { + throw new MealPlanningException("Invalid recipe ID", MealPlanningException.INVALID_RECIPE); + } + } + + private void validateEntryId(int entryId) { + if (entryId <= 0) { + throw new MealPlanningException("Invalid entry ID", MealPlanningException.INVALID_ENTRY); + } + } + + private void validateDate(LocalDate date) { + if (date == null) { + throw new MealPlanningException("Date cannot be null", MealPlanningException.INVALID_DATE); + } + } + + private void validateMealType(String mealType) { + if (mealType == null || mealType.trim().isEmpty()) { + throw new MealPlanningException("Meal type cannot be empty", MealPlanningException.INVALID_MEAL_TYPE); + } + } + + private void validateStatus(String status) { + if (status == null || status.trim().isEmpty()) { + throw new MealPlanningException("Status cannot be empty", MealPlanningException.INVALID_ENTRY); + } + } +} \ No newline at end of file diff --git a/src/main/java/use_case/meal_planning/MealPlanningOutputBoundary.java b/src/main/java/use_case/meal_planning/MealPlanningOutputBoundary.java new file mode 100644 index 000000000..e57f81584 --- /dev/null +++ b/src/main/java/use_case/meal_planning/MealPlanningOutputBoundary.java @@ -0,0 +1,55 @@ +package use_case.meal_planning; + +import java.util.List; + +import entity.MealPlanEntry; +import entity.Recipe; + +/** + * Output boundary interface for meal planning use case responses. + * Defines methods for presenting various meal planning operation results. + */ +public interface MealPlanningOutputBoundary { + + /** + * Presents the meal plan entries for a calendar week. + * + * @param entries list of meal plan entries to display + */ + void presentCalendarWeek(List entries); + + /** + * Presents a success message after adding a meal to the calendar. + * + * @param message success message to display + */ + void presentAddSuccess(String message); + + /** + * Presents a success message after removing a meal from the calendar. + * + * @param message success message to display + */ + void presentRemoveSuccess(String message); + + /** + * Presents a success message after updating a meal's status. + * + * @param message success message to display + */ + void presentStatusUpdateSuccess(String message); + + /** + * Presents an error message when an operation fails. + * + * @param error error message to display + */ + void presentError(String error); + + /** + * Presents the list of saved recipes. + * + * @param recipes list of recipes to display + */ + void presentSavedRecipes(List recipes); +} diff --git a/src/main/java/use_case/note/DataAccessException.java b/src/main/java/use_case/note/DataAccessException.java deleted file mode 100644 index b8c17920d..000000000 --- a/src/main/java/use_case/note/DataAccessException.java +++ /dev/null @@ -1,10 +0,0 @@ -package use_case.note; - -/** - * Exception thrown when there is an error with accessing data. - */ -public class DataAccessException extends Exception { - public DataAccessException(String string) { - super(string); - } -} diff --git a/src/main/java/use_case/note/NoteDataAccessInterface.java b/src/main/java/use_case/note/NoteDataAccessInterface.java deleted file mode 100644 index b71597828..000000000 --- a/src/main/java/use_case/note/NoteDataAccessInterface.java +++ /dev/null @@ -1,30 +0,0 @@ -package use_case.note; - -import entity.User; - -/** - * Interface for the NoteDAO. It consists of methods for - * both loading and saving a note. - */ -public interface NoteDataAccessInterface { - - /** - * Saves a note for a given user. This will replace any existing note. - *

The password of the user must match that of the user saved in the system.

- * @param user the user information associated with the note - * @param note the note to be saved - * @return the contents of the note - * @throws DataAccessException if the user's note can not be saved for any reason - */ - String saveNote(User user, String note) throws DataAccessException; - - /** - * Returns the note associated with the user. The password - * is not checked, so anyone can read the information. - * @param user the user information associated with the note - * @return the contents of the note - * @throws DataAccessException if the user's note can not be loaded for any reason - */ - String loadNote(User user) throws DataAccessException; - -} diff --git a/src/main/java/use_case/note/NoteInputBoundary.java b/src/main/java/use_case/note/NoteInputBoundary.java deleted file mode 100644 index b41da9bf5..000000000 --- a/src/main/java/use_case/note/NoteInputBoundary.java +++ /dev/null @@ -1,19 +0,0 @@ -package use_case.note; - -/** - * The Input Boundary for our note-related use cases. Since they are closely related, - * we have included them both in the same interface for simplicity. - */ -public interface NoteInputBoundary { - - /** - * Executes the refresh note use case. - */ - void executeRefresh(); - - /** - * Executes the save note use case. - * @param message the input data - */ - void executeSave(String message); -} diff --git a/src/main/java/use_case/note/NoteInteractor.java b/src/main/java/use_case/note/NoteInteractor.java deleted file mode 100644 index 369e9309a..000000000 --- a/src/main/java/use_case/note/NoteInteractor.java +++ /dev/null @@ -1,59 +0,0 @@ -package use_case.note; - -import entity.User; - -/** - * The "Use Case Interactor" for our two note-related use cases of refreshing - * the contents of the note and saving the contents of the note. Since they - * are closely related, we have combined them here for simplicity. - */ -public class NoteInteractor implements NoteInputBoundary { - - private final NoteDataAccessInterface noteDataAccessInterface; - private final NoteOutputBoundary noteOutputBoundary; - // Note: this program has it hardcoded which user object it is getting data for; - // you could change this if you wanted to generalize the code. For example, - // you might allow a user of the program to create a new note, which you - // could store as a "user" through the API OR you might maintain all notes - // in a JSON object stored in one common "user" stored through the API. - private final User user = new User("jonathan_calver2", "abc123"); - - public NoteInteractor(NoteDataAccessInterface noteDataAccessInterface, - NoteOutputBoundary noteOutputBoundary) { - this.noteDataAccessInterface = noteDataAccessInterface; - this.noteOutputBoundary = noteOutputBoundary; - } - - /** - * Executes the refresh note use case. - * - */ - @Override - public void executeRefresh() { - try { - - final String note = noteDataAccessInterface.loadNote(user); - noteOutputBoundary.prepareSuccessView(note); - } - catch (DataAccessException ex) { - noteOutputBoundary.prepareFailView(ex.getMessage()); - } - } - - /** - * Executes the save note use case. - * - * @param note the input data - */ - @Override - public void executeSave(String note) { - try { - - final String updatedNote = noteDataAccessInterface.saveNote(user, note); - noteOutputBoundary.prepareSuccessView(updatedNote); - } - catch (DataAccessException ex) { - noteOutputBoundary.prepareFailView(ex.getMessage()); - } - } -} diff --git a/src/main/java/use_case/note/NoteOutputBoundary.java b/src/main/java/use_case/note/NoteOutputBoundary.java deleted file mode 100644 index c0c2bb1d0..000000000 --- a/src/main/java/use_case/note/NoteOutputBoundary.java +++ /dev/null @@ -1,18 +0,0 @@ -package use_case.note; - -/** - * The output boundary for the Login Use Case. - */ -public interface NoteOutputBoundary { - /** - * Prepares the success view for the Note related Use Cases. - * @param message the output data - */ - void prepareSuccessView(String message); - - /** - * Prepares the failure view for the Note related Use Cases. - * @param errorMessage the explanation of the failure - */ - void prepareFailView(String errorMessage); -} diff --git a/src/main/java/use_case/nutrition_analysis/NutritionAnalysis.java b/src/main/java/use_case/nutrition_analysis/NutritionAnalysis.java new file mode 100644 index 000000000..bb2bf224e --- /dev/null +++ b/src/main/java/use_case/nutrition_analysis/NutritionAnalysis.java @@ -0,0 +1,17 @@ +package use_case.nutrition_analysis; + +import entity.Recipe; + +/** + * Interface for the Nutrition Analysis use case. + */ +public interface NutritionAnalysis { + + /** + * Searches for recipes based on a list of ingredients. + * + * @param recipe the title of recipe to analyze. + * @throws NutritionAnalysisException if search fails + */ + void analyzeNutrition(Recipe recipe) throws NutritionAnalysisException; +} diff --git a/src/main/java/use_case/nutrition_analysis/NutritionAnalysisDataAccessInterface.java b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisDataAccessInterface.java new file mode 100644 index 000000000..6a7d25e33 --- /dev/null +++ b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisDataAccessInterface.java @@ -0,0 +1,27 @@ +package use_case.nutrition_analysis; + +import entity.Nutrient; +import entity.Recipe; + +import java.io.IOException; +import java.util.List; + +/** + * Interface for accessing nutritional analysis data from an external API. + *

+ * Provides a method to analyze the nutritional content of a given recipe + * and return a list of Nutrient. + *

+ */ +public interface NutritionAnalysisDataAccessInterface { + /** + * Get totalNutrients from a POST request from the API. + * + * @param recipe the recipe we want to analyze the nutrition of. + * @return A list of Nutrient entity + * @throws IOException If an I/O error occurs. + */ + List analyzeNutrition(Recipe recipe) throws IOException; + // Helper methods are private and are not in the interface according to + // the encapsulation principle and interface Segregation (from SOLID). +} diff --git a/src/main/java/use_case/nutrition_analysis/NutritionAnalysisException.java b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisException.java new file mode 100644 index 000000000..5e79a3e82 --- /dev/null +++ b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisException.java @@ -0,0 +1,10 @@ +package use_case.nutrition_analysis; + +/** + * Exception class for nutrition analysis errors. + */ +public class NutritionAnalysisException extends RuntimeException { + public NutritionAnalysisException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/use_case/nutrition_analysis/NutritionAnalysisInteractor.java b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisInteractor.java new file mode 100644 index 000000000..4a59ac340 --- /dev/null +++ b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisInteractor.java @@ -0,0 +1,38 @@ +package use_case.nutrition_analysis; + +import java.io.IOException; +import java.util.List; + +import entity.Nutrient; +import entity.Recipe; + +/** + * Implementation of the Nutrition Analysis use case. + */ +public class NutritionAnalysisInteractor implements NutritionAnalysis { + + private final NutritionAnalysisDataAccessInterface nutritionAnalysisDAO; + private final NutritionAnalysisOutputBoundary outputBoundary; + + public NutritionAnalysisInteractor(NutritionAnalysisDataAccessInterface NutritionAnalysisDAO, + NutritionAnalysisOutputBoundary outputBoundary) { + this.nutritionAnalysisDAO = NutritionAnalysisDAO; + this.outputBoundary = outputBoundary; + } + + @Override + public void analyzeNutrition(Recipe recipe) throws NutritionAnalysisException { + try { + // Get NutritionInfo from Edamam API by the RecipeName. + final List nutritionInfo = nutritionAnalysisDAO.analyzeNutrition(recipe); + + // Present success + outputBoundary.presentNutritionInfo(nutritionInfo); + } + catch (IOException exception) { + // Present error + outputBoundary.presentError("Failed to analyze the recipe: " + exception.getMessage()); + throw new NutritionAnalysisException("Analysis failed", exception); + } + } +} diff --git a/src/main/java/use_case/nutrition_analysis/NutritionAnalysisOutputBoundary.java b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisOutputBoundary.java new file mode 100644 index 000000000..a13a4525e --- /dev/null +++ b/src/main/java/use_case/nutrition_analysis/NutritionAnalysisOutputBoundary.java @@ -0,0 +1,22 @@ +package use_case.nutrition_analysis; + +import java.util.List; + +import entity.Nutrient; + +/** + * Output boundary for the Nutrition Analysis use case. + */ +public interface NutritionAnalysisOutputBoundary { + /** + * Present the Nutrition information to the user. + * @param NutritionInfo List of recipes to present + */ + void presentNutritionInfo(List NutritionInfo); + + /** + * Present an error message to the user. + * @param error Error message to present + */ + void presentError(String error); +} diff --git a/src/main/java/use_case/recipe_search/RecipeSearchDataAccessInterface.java b/src/main/java/use_case/recipe_search/RecipeSearchDataAccessInterface.java new file mode 100644 index 000000000..15d3aa1d9 --- /dev/null +++ b/src/main/java/use_case/recipe_search/RecipeSearchDataAccessInterface.java @@ -0,0 +1,17 @@ +package use_case.recipe_search; + +import entity.Recipe; + +import java.io.IOException; +import java.util.List; + +public interface RecipeSearchDataAccessInterface { + /** + * Get searchedRecipe name from a POST request from the API. + * + * @param foodName the recipe name we want to display. + * @return A list of Recipe entity + * @throws IOException If an I/O error occurs. + */ + List searchRecipesByFoodName(String foodName); +} diff --git a/src/main/java/use_case/recipe_search/RecipeSearchException.java b/src/main/java/use_case/recipe_search/RecipeSearchException.java new file mode 100644 index 000000000..f9aac74f8 --- /dev/null +++ b/src/main/java/use_case/recipe_search/RecipeSearchException.java @@ -0,0 +1,11 @@ +package use_case.recipe_search; + +/** + * Exception class for recipe search errors. + */ +public class RecipeSearchException extends RuntimeException { + + public RecipeSearchException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/use_case/recipe_search/RecipeSearchInputBoundary.java b/src/main/java/use_case/recipe_search/RecipeSearchInputBoundary.java new file mode 100644 index 000000000..640c051e7 --- /dev/null +++ b/src/main/java/use_case/recipe_search/RecipeSearchInputBoundary.java @@ -0,0 +1,25 @@ +package use_case.recipe_search; + +import java.util.List; + +import entity.Recipe; + +/** + * Interface for the recipe search use case. + */ +public interface RecipeSearchInputBoundary { + /** + * Searches for recipes based on a list of ingredients. + * @param ingredients List of ingredients to search with + * @throws RecipeSearchException if search fails + */ + void searchRecipes(List ingredients) throws RecipeSearchException; + + /** + * Searches for recipes based on a list of ingredients. + * @param userId number represents user id. + * @param recipe the recipe related to user with user id. + * @throws RecipeSearchException if search fails + */ + void saveRecipe(int userId, Recipe recipe) throws RecipeSearchException; +} diff --git a/src/main/java/use_case/recipe_search/RecipeSearchInteractor.java b/src/main/java/use_case/recipe_search/RecipeSearchInteractor.java new file mode 100644 index 000000000..d06b08b34 --- /dev/null +++ b/src/main/java/use_case/recipe_search/RecipeSearchInteractor.java @@ -0,0 +1,82 @@ +package use_case.recipe_search; + +import java.util.ArrayList; +import java.util.List; + +import data_access.SavedRecipesDataAccessInterface; +import entity.Food; +import entity.Nutrition; +import entity.Recipe; + +public class RecipeSearchInteractor implements RecipeSearchInputBoundary { + private final RecipeSearchDataAccessInterface recipeSearchDataAccessInterface; + private final RecipeSearchOutputBoundary outputBoundary; + private final SavedRecipesDataAccessInterface savedRecipesDataAccessInterface; + + public RecipeSearchInteractor(RecipeSearchDataAccessInterface recipeSearchDataAccessInterface, + SavedRecipesDataAccessInterface savedRecipesDataAccessInterface, + RecipeSearchOutputBoundary outputBoundary) { + this.recipeSearchDataAccessInterface = recipeSearchDataAccessInterface; + this.savedRecipesDataAccessInterface = savedRecipesDataAccessInterface; + this.outputBoundary = outputBoundary; + } + + @Override + public void saveRecipe(int userId, Recipe recipe) throws RecipeSearchException { + try { + savedRecipesDataAccessInterface.saveRecipe(userId, recipe); + outputBoundary.presentSaveSuccess(recipe); + } + catch (Exception exception) { + outputBoundary.presentError("Failed to save recipe: " + exception.getMessage()); + throw new RecipeSearchException("Failed to save recipe", exception); + } + } + + @Override + public void searchRecipes(List ingredients) throws RecipeSearchException { + try { + final String searchQuery = String.join(",", ingredients); + final List searchResults = recipeSearchDataAccessInterface.searchRecipesByFoodName(searchQuery); + final List recipes = convertToRecipes(searchResults); + outputBoundary.presentRecipes(recipes); + } + catch (Exception exception) { + outputBoundary.presentError("Failed to search recipes: " + exception.getMessage()); + throw new RecipeSearchException("Recipe search failed", exception); + } + } + + private List convertToRecipes(List searchResults) { + final List recipes = new ArrayList<>(); + int recipeId = 1; + + for (Recipe result : searchResults) { + final String description = defaultIfNull(result.getDescription(), "No description available"); + final String instructions = defaultIfNull(result.getInstructions(), "Instructions not available"); + final Nutrition nutrition = defaultIfNull(result.getNutrition(), new Nutrition(0, 0, 0, 0, 0, 0)); + final List food = defaultIfNull(result.getFood(), new ArrayList<>()); + final int servings = Math.max(result.getServings(), 1); + + final Recipe recipe = new Recipe( + recipeId++, + result.getTitle(), + description, + result.getIngredients(), + instructions, + nutrition, + food, + result.getJsonIngredient(), + servings + ); + + recipes.add(recipe); + } + + return recipes; + } + + private T defaultIfNull(T value, T defaultValue) { + return value != null ? value : defaultValue; + } +} diff --git a/src/main/java/use_case/recipe_search/RecipeSearchOutputBoundary.java b/src/main/java/use_case/recipe_search/RecipeSearchOutputBoundary.java new file mode 100644 index 000000000..e0512ecb2 --- /dev/null +++ b/src/main/java/use_case/recipe_search/RecipeSearchOutputBoundary.java @@ -0,0 +1,24 @@ +package use_case.recipe_search; + +import entity.Recipe; +import java.util.List; + +public interface RecipeSearchOutputBoundary { + /** + * Present the recipes to the user. + * @param recipes List of recipes to present + */ + void presentRecipes(List recipes); + + /** + * Present an error message to the user. + * @param error Error message to present + */ + void presentError(String error); + + /** + * Present a success message when a recipe is saved. + * @param recipe The recipe that was successfully saved + */ + void presentSaveSuccess(Recipe recipe); +} \ No newline at end of file diff --git a/src/main/java/use_case/search_with_restriction/RecipeSearchWithRestrictionInteractor.java b/src/main/java/use_case/search_with_restriction/RecipeSearchWithRestrictionInteractor.java new file mode 100644 index 000000000..a3dd0e494 --- /dev/null +++ b/src/main/java/use_case/search_with_restriction/RecipeSearchWithRestrictionInteractor.java @@ -0,0 +1,91 @@ +package use_case.search_with_restriction; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import entity.Food; +import entity.Nutrition; +import entity.Recipe; + +/** + * Interactor for recipe search with restriction. + */ +public class RecipeSearchWithRestrictionInteractor implements SearchWithRestrictionInputBoundary { + private static final String DIET_LABEL = "Diet Label"; + private static final String HEALTH_LABEL = "Health Label"; + private static final String CUISINE_TYPE = "Cuisine Type"; + private static final String DELIMETER = ","; + + private final SearchWithRestrictionDataAccessInterface searchWithRestrictionDataAccessInterface; + private final SearchWithRestrictionOutputBoundary outputBoundary; + + public RecipeSearchWithRestrictionInteractor(SearchWithRestrictionDataAccessInterface searchWithRestrictionDataAccessInterface, + SearchWithRestrictionOutputBoundary outputBoundary) { + this.searchWithRestrictionDataAccessInterface = searchWithRestrictionDataAccessInterface; + this.outputBoundary = outputBoundary; + } + + @Override + public void searchRestrictionRecipes(Map> restrictions) throws SearchWithRestrictionException { + try { + final String searchFoodQuery = String.join(DELIMETER, restrictions.get("Food Name")); + + String searchDietQuery = null; + if (restrictions.get(DIET_LABEL) != null && !restrictions.get(DIET_LABEL).isEmpty()) { + searchDietQuery = String.join(DELIMETER, restrictions.get(DIET_LABEL)); + } + + String searchHealthQuery = null; + if (restrictions.get(HEALTH_LABEL) != null && !restrictions.get(HEALTH_LABEL).isEmpty()) { + searchHealthQuery = String.join(DELIMETER, restrictions.get(HEALTH_LABEL)); + } + + String searchCuisineQuery = null; + if (restrictions.get(CUISINE_TYPE) != null && !restrictions.get(CUISINE_TYPE).isEmpty()) { + searchCuisineQuery = String.join(DELIMETER, restrictions.get(CUISINE_TYPE)); + } + final List searchResults = searchWithRestrictionDataAccessInterface.searchRecipesByRestriction( + searchFoodQuery, searchDietQuery, searchHealthQuery, searchCuisineQuery); + final List recipes = convertToRecipes(searchResults); + outputBoundary.presentRecipes(recipes); + } + catch (Exception exception) { + outputBoundary.presentError("Failed to search recipes: " + exception.getMessage()); + throw new SearchWithRestrictionException("Recipe search failed", exception); + } + } + + private List convertToRecipes(List searchResults) { + final List recipes = new ArrayList<>(); + int recipeId = 1; + + for (Recipe result : searchResults) { + final String description = defaultIfNull(result.getDescription(), "No description available"); + final String instructions = defaultIfNull(result.getInstructions(), "Instructions not available"); + final Nutrition nutrition = defaultIfNull(result.getNutrition(), new Nutrition(0, 0, 0, 0, 0, 0)); + final List food = defaultIfNull(result.getFood(), new ArrayList<>()); + final int servings = Math.max(result.getServings(), 1); + + final Recipe recipe = new Recipe( + recipeId++, + result.getTitle(), + description, + result.getIngredients(), + instructions, + nutrition, + food, + result.getJsonIngredient(), + servings + ); + + recipes.add(recipe); + } + + return recipes; + } + + private T defaultIfNull(T value, T defaultValue) { + return value != null ? value : defaultValue; + } +} diff --git a/src/main/java/use_case/search_with_restriction/SearchWithRestrictionDataAccessInterface.java b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionDataAccessInterface.java new file mode 100644 index 000000000..da6008d5e --- /dev/null +++ b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionDataAccessInterface.java @@ -0,0 +1,19 @@ +package use_case.search_with_restriction; + +import entity.Recipe; + +import java.util.List; + +public interface SearchWithRestrictionDataAccessInterface { + /** + * Get searchedRecipe name from a POST request from the API. + * + * @param foodName the ingredient name from user's input. + * @param diet the diet plan user chose. + * @param health the health plan user chose. + * @param cuisineType the cuisine type user chose. + * @return A list of Recipe entity + */ + List searchRecipesByRestriction(String foodName, String diet, String health, String + cuisineType); +} diff --git a/src/main/java/use_case/search_with_restriction/SearchWithRestrictionException.java b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionException.java new file mode 100644 index 000000000..f7c40733a --- /dev/null +++ b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionException.java @@ -0,0 +1,10 @@ +package use_case.search_with_restriction; + +/** + * Exception class for recipe search with restriction errors. + */ +public class SearchWithRestrictionException extends RuntimeException { + public SearchWithRestrictionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/use_case/search_with_restriction/SearchWithRestrictionInputBoundary.java b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionInputBoundary.java new file mode 100644 index 000000000..7e2f53549 --- /dev/null +++ b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionInputBoundary.java @@ -0,0 +1,19 @@ +package use_case.search_with_restriction; + +import java.util.List; +import java.util.Map; + +import use_case.recipe_search.RecipeSearchException; + +/** + * Interface for the recipe search with restriction use case. + */ +public interface SearchWithRestrictionInputBoundary { + /** + * Searches for recipes based on a map of restrictions. + * @param restrictions Map of restrictions to search with + * @throws RecipeSearchException if search fails + */ + void searchRestrictionRecipes(Map> restrictions) throws RecipeSearchException; + +} diff --git a/src/main/java/use_case/search_with_restriction/SearchWithRestrictionOutputBoundary.java b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionOutputBoundary.java new file mode 100644 index 000000000..43e3cc62d --- /dev/null +++ b/src/main/java/use_case/search_with_restriction/SearchWithRestrictionOutputBoundary.java @@ -0,0 +1,22 @@ +package use_case.search_with_restriction; + +import java.util.List; + +import entity.Recipe; + +/** + * The outputBoundary for SearchWithRestriction. + */ +public interface SearchWithRestrictionOutputBoundary { + /** + * Present the recipes to the user. + * @param recipes List of recipes to present + */ + void presentRecipes(List recipes); + + /** + * Present an error message to the user. + * @param error Error message to present + */ + void presentError(String error); +} diff --git a/src/main/java/use_case/serving_adjust/ServingAdjust.java b/src/main/java/use_case/serving_adjust/ServingAdjust.java new file mode 100644 index 000000000..3de07e7c7 --- /dev/null +++ b/src/main/java/use_case/serving_adjust/ServingAdjust.java @@ -0,0 +1,26 @@ +package use_case.serving_adjust; + +import java.util.List; + +import entity.Recipe; + +/** + * Interface for serving adjustment use case. + */ +public interface ServingAdjust { + /** + * Adjust the servings for a single recipe. + * + * @param newServings New number of servings + * @param recipe The recipe to adjust + */ + void adjustServings(int newServings, Recipe recipe); + + /** + * Adjust the servings for multiple recipes. + * + * @param newServings New number of servings + * @param recipes List of recipes to adjust + */ + void adjustServingsForMultiple(int newServings, List recipes); +} diff --git a/src/main/java/use_case/serving_adjust/ServingAdjustInputBoundary.java b/src/main/java/use_case/serving_adjust/ServingAdjustInputBoundary.java new file mode 100644 index 000000000..1353f38c7 --- /dev/null +++ b/src/main/java/use_case/serving_adjust/ServingAdjustInputBoundary.java @@ -0,0 +1,18 @@ +package use_case.serving_adjust; + +import java.util.List; + +import entity.Recipe; + +/** + * Interface for serving adjustment use case input. + */ +public interface ServingAdjustInputBoundary { + /** + * Adjust the servings for a list of recipes. + * + * @param servings The number of servings to set. + * @param recipes The list of recipes to update. + */ + void adjustServings(int servings, List recipes); +} diff --git a/src/main/java/use_case/serving_adjust/ServingAdjustInteractor.java b/src/main/java/use_case/serving_adjust/ServingAdjustInteractor.java new file mode 100644 index 000000000..fc1e16c05 --- /dev/null +++ b/src/main/java/use_case/serving_adjust/ServingAdjustInteractor.java @@ -0,0 +1,72 @@ +package use_case.serving_adjust; + +import java.util.List; + +import entity.Recipe; + +/** + * Implementation of the serving adjustment use case. + */ +public class ServingAdjustInteractor implements ServingAdjust, ServingAdjustInputBoundary { + + private final ServingAdjustOutputBoundary outputBoundary; + + /** + * Constructs a ServingAdjustInteractor with the specified output boundary. + * + * @param outputBoundary The output boundary to handle the results of the serving adjustment. + */ + public ServingAdjustInteractor(ServingAdjustOutputBoundary outputBoundary) { + this.outputBoundary = outputBoundary; + } + + /** + * Adjusts the servings for a single recipe. + * + * @param newServings The new number of servings. Must be greater than zero. + * @param recipe The recipe to adjust. Must not be null. + * @throws IllegalArgumentException if {@code newServings} is less than or equal to zero.i + */ + @Override + public void adjustServings(int newServings, Recipe recipe) { + if (newServings <= 0) { + throw new IllegalArgumentException("Servings must be greater than zero."); + } + + final int currentServings = recipe.getServings(); + final double factor = (double) newServings / currentServings; + + recipe.getIngredients().forEach(ingredient -> { + ingredient.setQuantity(ingredient.getQuantity() * factor); + }); + + recipe.setServings(newServings); + } + + /** + * Adjusts the servings for a list of recipes. + * + * @param servings The new number of servings. Must be greater than zero. + * @param recipes The list of recipes to adjust. Must not be null or empty. + * @throws IllegalArgumentException if {@code servings} is less than or equal to zero. + */ + @Override + public void adjustServings(int servings, List recipes) { + adjustServingsForMultiple(servings, recipes); + } + + /** + * Adjusts the servings for multiple recipes. + * + * @param newServings The new number of servings. Must be greater than zero. + * @param recipes The list of recipes to adjust. Must not be null or empty. + * @throws IllegalArgumentException if {@code newServings} is less than or equal to zero. + */ + @Override + public void adjustServingsForMultiple(int newServings, List recipes) { + for (Recipe recipe : recipes) { + adjustServings(newServings, recipe); + } + outputBoundary.presentUpdatedRecipes(recipes); + } +} diff --git a/src/main/java/use_case/serving_adjust/ServingAdjustOutputBoundary.java b/src/main/java/use_case/serving_adjust/ServingAdjustOutputBoundary.java new file mode 100644 index 000000000..504bb009b --- /dev/null +++ b/src/main/java/use_case/serving_adjust/ServingAdjustOutputBoundary.java @@ -0,0 +1,17 @@ +package use_case.serving_adjust; + +import java.util.List; + +import entity.Recipe; + +/** + * Interface for presenting the output of serving adjustments. + */ +public interface ServingAdjustOutputBoundary { + /** + * Present the updated recipes after servings are adjusted. + * + * @param updatedRecipes The list of updated recipes. + */ + void presentUpdatedRecipes(List updatedRecipes); +} diff --git a/src/main/java/use_case/signup/SignupInputBoundary.java b/src/main/java/use_case/signup/SignupInputBoundary.java new file mode 100644 index 000000000..1cb69e02e --- /dev/null +++ b/src/main/java/use_case/signup/SignupInputBoundary.java @@ -0,0 +1,18 @@ +package use_case.signup; + +/** + * Input Boundary for actions which are related to signing up. + */ +public interface SignupInputBoundary { + + /** + * Executes the signup use case. + * @param signupInputData the input data + */ + void execute(SignupInputData signupInputData); + + /** + * Executes the switch to login view use case. + */ + void switchToLoginView(); +} diff --git a/src/main/java/use_case/signup/SignupInputData.java b/src/main/java/use_case/signup/SignupInputData.java new file mode 100644 index 000000000..86c5e8abc --- /dev/null +++ b/src/main/java/use_case/signup/SignupInputData.java @@ -0,0 +1,29 @@ +package use_case.signup; + +/** + * The Input Data for the Signup Use Case. + */ +public class SignupInputData { + + private final String username; + private final String password; + private final String repeatPassword; + + public SignupInputData(String username, String password, String repeatPassword) { + this.username = username; + this.password = password; + this.repeatPassword = repeatPassword; + } + + String getUsername() { + return username; + } + + String getPassword() { + return password; + } + + public String getRepeatPassword() { + return repeatPassword; + } +} diff --git a/src/main/java/use_case/signup/SignupInteractor.java b/src/main/java/use_case/signup/SignupInteractor.java new file mode 100644 index 000000000..3fd6560c7 --- /dev/null +++ b/src/main/java/use_case/signup/SignupInteractor.java @@ -0,0 +1,43 @@ +package use_case.signup; + +import entity.User; +import entity.UserFactory; + +/** + * The Signup Interactor. + */ +public class SignupInteractor implements SignupInputBoundary { + private final SignupUserDataAccessInterface userDataAccessObject; + private final SignupOutputBoundary userPresenter; + private final UserFactory userFactory; + + public SignupInteractor(SignupUserDataAccessInterface signupDataAccessInterface, + SignupOutputBoundary signupOutputBoundary, + UserFactory userFactory) { + this.userDataAccessObject = signupDataAccessInterface; + this.userPresenter = signupOutputBoundary; + this.userFactory = userFactory; + } + + @Override + public void execute(SignupInputData signupInputData) { + if (userDataAccessObject.existsByName(signupInputData.getUsername())) { + userPresenter.prepareFailView("User already exists."); + } + else if (!signupInputData.getPassword().equals(signupInputData.getRepeatPassword())) { + userPresenter.prepareFailView("Passwords don't match."); + } + else { + final User user = userFactory.create(signupInputData.getUsername(), signupInputData.getPassword()); + userDataAccessObject.save(user); + + final SignupOutputData signupOutputData = new SignupOutputData(user.getName(), false); + userPresenter.prepareSuccessView(signupOutputData); + } + } + + @Override + public void switchToLoginView() { + userPresenter.switchToLoginView(); + } +} diff --git a/src/main/java/use_case/signup/SignupOutputBoundary.java b/src/main/java/use_case/signup/SignupOutputBoundary.java new file mode 100644 index 000000000..314376b93 --- /dev/null +++ b/src/main/java/use_case/signup/SignupOutputBoundary.java @@ -0,0 +1,24 @@ +package use_case.signup; + +/** + * The output boundary for the Signup Use Case. + */ +public interface SignupOutputBoundary { + + /** + * Prepares the success view for the Signup Use Case. + * @param outputData the output data + */ + void prepareSuccessView(SignupOutputData outputData); + + /** + * Prepares the failure view for the Signup Use Case. + * @param errorMessage the explanation of the failure + */ + void prepareFailView(String errorMessage); + + /** + * Switches to the Login View. + */ + void switchToLoginView(); +} diff --git a/src/main/java/use_case/signup/SignupOutputData.java b/src/main/java/use_case/signup/SignupOutputData.java new file mode 100644 index 000000000..6dc74d2fb --- /dev/null +++ b/src/main/java/use_case/signup/SignupOutputData.java @@ -0,0 +1,24 @@ +package use_case.signup; + +/** + * Output Data for the Signup Use Case. + */ +public class SignupOutputData { + + private final String username; + + private final boolean useCaseFailed; + + public SignupOutputData(String username, boolean useCaseFailed) { + this.username = username; + this.useCaseFailed = useCaseFailed; + } + + public String getUsername() { + return username; + } + + public boolean isUseCaseFailed() { + return useCaseFailed; + } +} diff --git a/src/main/java/use_case/signup/SignupUserDataAccessInterface.java b/src/main/java/use_case/signup/SignupUserDataAccessInterface.java new file mode 100644 index 000000000..b9d60f585 --- /dev/null +++ b/src/main/java/use_case/signup/SignupUserDataAccessInterface.java @@ -0,0 +1,22 @@ +package use_case.signup; + +import entity.User; + +/** + * DAO for the Signup Use Case. + */ +public interface SignupUserDataAccessInterface { + + /** + * Checks if the given username exists. + * @param username the username to look for + * @return true if a user with the given username exists; false otherwise + */ + boolean existsByName(String username); + + /** + * Saves the user. + * @param user the user to save + */ + void save(User user); +} diff --git a/src/main/java/view/CalendarPanel.java b/src/main/java/view/CalendarPanel.java new file mode 100644 index 000000000..69a2b4ee6 --- /dev/null +++ b/src/main/java/view/CalendarPanel.java @@ -0,0 +1,176 @@ +package view; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridLayout; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +import entity.MealPlanEntry; +import entity.Recipe; +import interface_adapter.meal_planning.MealPlanningController; +import interface_adapter.meal_planning.MealPlanningState; + +/** + * Panel for displaying and managing meal planning calendar. + * Supports adding meals, updating their status, and removing entries. + */ + +public class CalendarPanel extends JPanel { + + private final SavedRecipesPanel savedRecipesList; + private final JPanel gridPanel; + private MealPlanningController controller; + + public CalendarPanel(SavedRecipesPanel savedRecipesList) { + this.savedRecipesList = savedRecipesList; + setLayout(new BorderLayout()); + gridPanel = new JPanel(new GridLayout(0, ViewConstants.DAYS_IN_WEEK, ViewConstants + .HORIZONTAL_GAP, ViewConstants.VERTICAL_GAP)); + gridPanel.setBorder(BorderFactory.createEtchedBorder()); + add(createHeaderPanel(), BorderLayout.NORTH); + add(gridPanel, BorderLayout.CENTER); + } + + private JPanel createHeaderPanel() { + final JPanel header = new JPanel(new GridLayout(1, ViewConstants.DAYS_IN_WEEK)); + for (String day : new String[]{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}) { + header.add(new JLabel(day, SwingConstants.CENTER)); + } + return header; + } + + void refresh(MealPlanningState state) { + gridPanel.removeAll(); + if (state != null) { + LocalDate date = state.getCurrentWeekStart(); + for (int i = 0; i < ViewConstants.DAYS_IN_WEEK; i++) { + gridPanel.add(createDayPanel(date, state.getMealPlanEntries())); + date = date.plusDays(1); + } + } + revalidate(); + repaint(); + } + + private JPanel createDayPanel(LocalDate date, List entries) { + final JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createEtchedBorder()); + + addDateLabel(panel, date); + addMealEntries(panel, date, entries); + addAddButton(panel, date); + + return panel; + } + + private void addDateLabel(JPanel panel, LocalDate date) { + final JLabel label = new JLabel(date.format(DateTimeFormatter.ofPattern("MM/dd"))); + label.setAlignmentX(Component.CENTER_ALIGNMENT); + if (date.equals(LocalDate.now())) { + label.setForeground(Color.BLUE); + label.setFont(label.getFont().deriveFont(Font.BOLD)); + } + panel.add(label); + } + + private void addMealEntries(JPanel panel, LocalDate date, List entries) { + entries.stream() + .filter(entry -> entry.getDate().equals(date)) + .forEach(entry -> panel.add(createMealPanel(entry))); + } + + private JPanel createMealPanel(MealPlanEntry entry) { + final JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + panel.setBackground(getStatusColor(entry.getStatus())); + + Arrays.asList( + new JLabel(entry.getRecipe().getTitle()), + new JLabel(entry.getMealType()), + createStatusComboBox(entry), + createRemoveButton(entry) + ).forEach(component -> { + component.setAlignmentX(Component.CENTER_ALIGNMENT); + panel.add(component); + }); + + return panel; + } + + private JComboBox createStatusComboBox(MealPlanEntry entry) { + final JComboBox combo = new JComboBox<>(ViewConstants.MEAL_STATUSES); + combo.setSelectedItem(ViewUtils.capitalizeFirstLetter(entry.getStatus())); + combo.addActionListener(actionEvent -> { + controller.updateMealStatus( + ViewUtils.getCurrentUserId(), + entry.getEntryId(), + ((String) combo.getSelectedItem()).toLowerCase() + ); + }); + return combo; + } + + private JButton createRemoveButton(MealPlanEntry entry) { + final JButton button = new JButton("\u00D7"); + button.setActionCommand("REMOVE_" + entry.getEntryId()); + button.addActionListener(actionEvent -> { + controller.removeFromCalendar(ViewUtils.getCurrentUserId(), entry.getEntryId()); + controller.viewCalendarWeek(ViewUtils.getCurrentUserId(), entry.getDate()); + }); + return button; + } + + private void addAddButton(JPanel panel, LocalDate date) { + final JButton button = new JButton("+"); + button.setPreferredSize(new Dimension(ViewConstants.BUTTON_WIDTH, ViewConstants.BUTTON_HEIGHT)); + button.addActionListener(actionEvent -> handleAddMeal(date)); + + final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + buttonPanel.add(button); + panel.add(buttonPanel); + } + + private Color getStatusColor(String status) { + return switch (status.toLowerCase()) { + case "completed" -> ViewConstants.COMPLETED_COLOR; + case "in progress" -> ViewConstants.IN_PROGRESS_COLOR; + default -> ViewConstants.DEFAULT_COLOR; + }; + } + + void setController(MealPlanningController controller) { + this.controller = controller; + } + + private void handleAddMeal(LocalDate date) { + final Recipe selectedRecipe = savedRecipesList.getSelectedRecipe(); + final String selectedMealType = savedRecipesList.getSelectedMealType(); + + if (selectedRecipe != null && controller != null) { + controller.addToCalendar( + ViewUtils.getCurrentUserId(), + selectedRecipe.getRecipeId(), + date, + selectedMealType + ); + controller.viewCalendarWeek(ViewUtils.getCurrentUserId(), date); + } + } +} diff --git a/src/main/java/view/FilterFrameView.java b/src/main/java/view/FilterFrameView.java new file mode 100644 index 000000000..ad34ca1ec --- /dev/null +++ b/src/main/java/view/FilterFrameView.java @@ -0,0 +1,187 @@ +package view; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FilterFrameView extends JFrame { + private static final int WINDOW_WIDTH = 500; + private static final int WINDOW_HEIGHT = 500; + + private final RecipeSearchView mainFrame; + private final JTextArea resultArea; + + private final JPanel dietPanel; + private final JPanel healthPanel; + private final JPanel cuisinePanel; + + private Map> restrictionMap; + + private static final String[] DIET_TYPES = {"balanced", "high-fiber", "high-protein", "low-carb", "low-fat", "low-sodium"}; + private static final String[] DIET_RESTRICTIONS = {"alcohol-cocktail", "alcohol-free", "celery-free", "crustacean-free", + "dairy-free", "DASH", "egg-free", "fish-free", "fodmap-free", "gluten-free", "immuno-supportive", "keto-friendly", + "kidney-friendly", "kosher", "low-fat-abs", "low-potassium", "low-sugar", "lupine-free", "Mediterranean", "mollusk-free", + "mustard-free", "no-oil-added", "paleo", "peanut-free", "pescatarian", "pork-free", "red-meat-free", "sesame-free", + "shellfish-free", "soy-free", "sugar-conscious", "sulfite-free", "tree-nut-free", "vegan", "vegetarian", "wheat-free"}; + private static final String[] CUISINE_TYPES = {"American", "Asian", "British", "Caribbean", "Central Europe", "Chinese", + "Eastern Europe", "French", "Indian", "Italian", "Japanese", "Kosher", "Mediterranean", "Mexican", "Middle Eastern", + "Nordic", "South American", "South East Asian"}; + + public FilterFrameView(RecipeSearchView mainFrame) { + this.mainFrame = mainFrame; + + restrictionMap = mainFrame.getRestrictionMap(); + + setTitle("Filter Page"); + setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLocationRelativeTo(null); + + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout(10, 10)); + + JLabel label = new JLabel("Select Filters:", JLabel.CENTER); + label.setFont(new Font("Arial", Font.PLAIN, 16)); + panel.add(label, BorderLayout.NORTH); + + JPanel checkBoxPanel = new JPanel(); + checkBoxPanel.setLayout(new GridLayout(3, 1, 10, 10)); + + dietPanel = createScrollableCheckBoxPanel("Diet Label", DIET_TYPES); + healthPanel = createScrollableCheckBoxPanel("Health Label", DIET_RESTRICTIONS); + cuisinePanel = createScrollableCheckBoxPanel("Cuisine Type", CUISINE_TYPES); + + checkBoxPanel.add(dietPanel); + checkBoxPanel.add(healthPanel); + checkBoxPanel.add(cuisinePanel); + + panel.add(checkBoxPanel, BorderLayout.CENTER); + + resultArea = new JTextArea(5, 30); + resultArea.setEditable(false); + resultArea.setFont(new Font("Arial", Font.PLAIN, 14)); + panel.add(new JScrollPane(resultArea), BorderLayout.SOUTH); + + JPanel buttonPanel = new JPanel(); + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> cancelAction()); + JButton confirmButton = new JButton("Confirm"); + confirmButton.addActionListener(e -> confirmAction()); + + buttonPanel.add(cancelButton); + buttonPanel.add(confirmButton); + + panel.add(buttonPanel, BorderLayout.SOUTH); + + add(panel); + setVisible(true); + } + + private JPanel createScrollableCheckBoxPanel(String label, String[] items) { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createTitledBorder(label)); + + for (String item : items) { + panel.add(new JCheckBox(item)); + } + + JScrollPane scrollPane = new JScrollPane(panel); + scrollPane.setPreferredSize(new Dimension(400, 120)); + + JPanel wrappedPanel = new JPanel(new BorderLayout()); + wrappedPanel.add(scrollPane, BorderLayout.CENTER); + + return wrappedPanel; + } + + private JCheckBox[] getCheckBoxes(JPanel panel) { + for (Component component : panel.getComponents()) { + if (component instanceof JScrollPane) { + JPanel innerPanel = (JPanel) ((JScrollPane) component).getViewport().getView(); + Component[] components = innerPanel.getComponents(); + JCheckBox[] checkBoxes = new JCheckBox[components.length]; + for (int i = 0; i < components.length; i++) { + checkBoxes[i] = (JCheckBox) components[i]; + } + return checkBoxes; + } + } + return new JCheckBox[0]; + } + + private String getSelectedFilters() { + StringBuilder resultText = new StringBuilder(); + + appendSelectedFilters(resultText, dietPanel); + appendSelectedFilters(resultText, healthPanel); + appendSelectedFilters(resultText, cuisinePanel); + + return resultText.toString(); + } + + // Helper Method + private void appendSelectedFilters(StringBuilder resultText, JPanel panel) { + boolean first = true; + for (JCheckBox checkBox : getCheckBoxes(panel)) { + if (checkBox.isSelected()) { + if (!first) { + resultText.append(", "); + } + resultText.append(checkBox.getText()); + first = false; + } + } + resultText.append("\n"); + } + + private List getDietType() { + ArrayList dietList = new ArrayList<>(); + for (JCheckBox checkBox : getCheckBoxes(dietPanel)) { + if (checkBox.isSelected()) { + dietList.add(checkBox.getText()); + } + } + return dietList; + } + + private List getHealthType() { + ArrayList healthList = new ArrayList<>(); + for (JCheckBox checkBox : getCheckBoxes(healthPanel)) { + if (checkBox.isSelected()) { + healthList.add(checkBox.getText()); + } + } + return healthList; + } + + private List getCuisineType() { + ArrayList cuisineList = new ArrayList<>(); + for (JCheckBox checkBox : getCheckBoxes(cuisinePanel)) { + if (checkBox.isSelected()) { + cuisineList.add(checkBox.getText()); + } + } + return cuisineList; + } + + private void dataTransfer() { + restrictionMap.put("Diet Types", getDietType()); + restrictionMap.put("Health Types", getHealthType()); + restrictionMap.put("Cuisine Types", getCuisineType()); + } + + private void cancelAction() { + mainFrame.setVisible(true); + this.setVisible(false); + } + + private void confirmAction() { + String selectedFilters = getSelectedFilters(); + mainFrame.updateSelection(selectedFilters); + dataTransfer(); + this.setVisible(false); + } +} diff --git a/src/main/java/view/FrontPageView.java b/src/main/java/view/FrontPageView.java new file mode 100644 index 000000000..0f754fc7e --- /dev/null +++ b/src/main/java/view/FrontPageView.java @@ -0,0 +1,74 @@ +package view; + +import interface_adapter.start.FrontPageController; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class FrontPageView extends JFrame implements ActionListener { + + private final JButton buttonA; + private final JButton buttonB; + + private final RecipeSearchView recipeSearchView; + private final MealPlanningView mealPlanningView; + + public FrontPageView(RecipeSearchView recipeSearchView, MealPlanningView mealPlanningView) { + super("Start Page"); + + this.recipeSearchView = recipeSearchView; + this.mealPlanningView = mealPlanningView; + + setLayout(new BorderLayout()); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(1500, 900); + setLocationRelativeTo(null); + + JPanel panel = new ImagePanel(); + panel.setLayout(new BorderLayout()); + panel.setOpaque(false); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 20, 0)); + buttonPanel.setOpaque(false); + + buttonA = new JButton("Recipe Search"); + buttonB = new JButton("Meal Planner"); + + buttonA.setFont(new Font("Arial", Font.BOLD, 16)); + buttonB.setFont(new Font("Arial", Font.BOLD, 16)); + buttonA.setPreferredSize(new Dimension(140, 50)); + buttonB.setPreferredSize(new Dimension(140, 50)); + + buttonA.addActionListener(this); + buttonB.addActionListener(this); + + buttonPanel.add(buttonA); + buttonPanel.add(buttonB); + + JPanel topPaddingPanel = new JPanel(); + topPaddingPanel.setPreferredSize(new Dimension(400, 500)); + topPaddingPanel.setOpaque(false); + + panel.add(topPaddingPanel, BorderLayout.NORTH); + panel.add(buttonPanel, BorderLayout.CENTER); + + add(panel, BorderLayout.CENTER); + + setVisible(true); + } + + @Override + public void actionPerformed(ActionEvent evt) { + if (evt.getSource().equals(buttonA)) { + setVisible(false); + FrontPageController.goToRecipeSearch(recipeSearchView, mealPlanningView); + } + else if (evt.getSource().equals(buttonB)) { + setVisible(false); + FrontPageController.goToMealPlanning(recipeSearchView, mealPlanningView); + } + } +} diff --git a/src/main/java/view/ImagePanel.java b/src/main/java/view/ImagePanel.java new file mode 100644 index 000000000..db933fda1 --- /dev/null +++ b/src/main/java/view/ImagePanel.java @@ -0,0 +1,28 @@ +package view; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; + +class ImagePanel extends JPanel { + private Image backgroundImage; + + public ImagePanel() { + try { + File file = new File("src/main/java/resources/img_1.png"); + backgroundImage = ImageIO.read(file); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (backgroundImage != null) { + g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), this); + } + } +} \ No newline at end of file diff --git a/src/main/java/view/MealPlanningView.java b/src/main/java/view/MealPlanningView.java new file mode 100644 index 000000000..dd3b39d97 --- /dev/null +++ b/src/main/java/view/MealPlanningView.java @@ -0,0 +1,167 @@ +package view; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.time.LocalDate; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.border.EmptyBorder; + +import interface_adapter.meal_planning.MealPlanningController; +import interface_adapter.meal_planning.MealPlanningState; +import interface_adapter.meal_planning.MealPlanningViewModel; + +/** + * View for the meal planning use case. + * Manages calendar display, recipe selection, and week navigation. + */ + +public class MealPlanningView extends JPanel implements ActionListener, PropertyChangeListener { + private final MealPlanningViewModel viewModel; + private MealPlanningController controller; + private final CalendarPanel calendarPanel; + private final SavedRecipesPanel savedRecipesPanel; + private final WeekNavigationPanel navigationPanel; + + /** + * Creates a new meal planning view. + * + * @param viewModel The view model containing meal planning data + */ + public MealPlanningView(MealPlanningViewModel viewModel) { + this.viewModel = viewModel; + this.viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout(ViewConstants.HORIZONTAL_GAP, ViewConstants.VERTICAL_GAP)); + setBorder(new EmptyBorder(ViewConstants.PLANNING_BORDER_PADDING, ViewConstants.PLANNING_BORDER_PADDING, + ViewConstants.PLANNING_BORDER_PADDING, ViewConstants.PLANNING_BORDER_PADDING)); + + this.navigationPanel = new WeekNavigationPanel(this); + this.savedRecipesPanel = new SavedRecipesPanel(); + this.calendarPanel = new CalendarPanel(this.savedRecipesPanel); + + initializeLayout(); + } + + private void initializeLayout() { + final JLabel titleLabel = ViewUtils.createTitleLabel(viewModel.getViewName()); + + final JPanel contentPanel = new JPanel(new BorderLayout(ViewConstants.HORIZONTAL_GAP, ViewConstants + .VERTICAL_GAP)); + contentPanel.add(navigationPanel, BorderLayout.NORTH); + contentPanel.add(new JScrollPane(calendarPanel), BorderLayout.CENTER); + + add(titleLabel, BorderLayout.NORTH); + add(contentPanel, BorderLayout.CENTER); + add(savedRecipesPanel, BorderLayout.EAST); + + initializeCalendar(); + } + + private void initializeCalendar() { + final MealPlanningState initialState = new MealPlanningState(); + final LocalDate weekStart = ViewUtils.getCurrentWeekStart(); + initialState.setCurrentWeekStart(weekStart); + viewModel.setState(initialState); + refreshDisplay(); + } + + /** + * Updates all view components with current state data. + */ + public void refreshDisplay() { + calendarPanel.refresh(viewModel.getState()); + navigationPanel.updateWeekLabel(viewModel.getState()); + savedRecipesPanel.updateRecipesList(viewModel.getState().getSavedRecipes()); + revalidate(); + repaint(); + } + + @Override + public void actionPerformed(ActionEvent evt) { + if (controller != null && viewModel.getState() != null) { + final String command = evt.getActionCommand(); + handleAction(command); + } + } + + private void handleAction(String command) { + final MealPlanningState state = viewModel.getState(); + try { + switch (command) { + case "Previous Week": + handleWeekNavigation(state.getCurrentWeekStart().minusWeeks(1)); + break; + case "Next Week": + handleWeekNavigation(state.getCurrentWeekStart().plusWeeks(1)); + break; + default: + if (command.startsWith("REMOVE_")) { + handleRemoveEntry(command, state); + } + } + } + catch (IllegalArgumentException exception) { + ViewUtils.showErrorDialog(this, "Operation failed", exception); + } + } + + private void handleWeekNavigation(LocalDate newStart) { + viewModel.getState().setCurrentWeekStart(newStart); + controller.viewCalendarWeek(ViewUtils.getCurrentUserId(), newStart); + } + + private void handleRemoveEntry(String command, MealPlanningState state) { + final int entryId = Integer.parseInt(command.substring(7)); + controller.removeFromCalendar(ViewUtils.getCurrentUserId(), entryId); + controller.viewCalendarWeek(ViewUtils.getCurrentUserId(), state.getCurrentWeekStart()); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + final MealPlanningState state = (MealPlanningState) evt.getNewValue(); + if (state != null) { + ViewUtils.handleStateChange(this, state); + refreshDisplay(); + } + } + + /** + * Sets the controller and initializes meal planning data. + * + * @param controller The meal planning controller + */ + public void setController(MealPlanningController controller) { + this.controller = controller; + this.calendarPanel.setController(controller); + this.savedRecipesPanel.setController(controller); + + if (controller != null) { + controller.initializeMealPlanning(ViewUtils.getCurrentUserId()); + controller.viewCalendarWeek(ViewUtils.getCurrentUserId(), viewModel.getState().getCurrentWeekStart()); + } + } + + /** + * Gets the current controller instance. + * + * @return The meal planning controller + */ + public MealPlanningController getController() { + return this.controller; + } + + /** + * Refreshes the saved recipes list from the data source. + */ + public void refreshSavedRecipes() { + if (controller != null) { + controller.getSavedRecipes(ViewUtils.getCurrentUserId()); + } + } +} diff --git a/src/main/java/view/NoteView.java b/src/main/java/view/NoteView.java deleted file mode 100644 index 331d76493..000000000 --- a/src/main/java/view/NoteView.java +++ /dev/null @@ -1,95 +0,0 @@ -package view; - -import java.awt.Component; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; - -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JTextArea; - -import interface_adapter.note.NoteController; -import interface_adapter.note.NoteState; -import interface_adapter.note.NoteViewModel; - -/** - * The View for when the user is viewing a note in the program. - */ -public class NoteView extends JPanel implements ActionListener, PropertyChangeListener { - - private final NoteViewModel noteViewModel; - - private final JLabel noteName = new JLabel("note for jonathan_calver2"); - private final JTextArea noteInputField = new JTextArea(); - - private final JButton saveButton = new JButton("Save"); - private final JButton refreshButton = new JButton("Refresh"); - private NoteController noteController; - - public NoteView(NoteViewModel noteViewModel) { - - noteName.setAlignmentX(Component.CENTER_ALIGNMENT); - this.noteViewModel = noteViewModel; - this.noteViewModel.addPropertyChangeListener(this); - - final JPanel buttons = new JPanel(); - buttons.add(saveButton); - buttons.add(refreshButton); - - saveButton.addActionListener( - evt -> { - if (evt.getSource().equals(saveButton)) { - noteController.execute(noteInputField.getText()); - - } - } - ); - - refreshButton.addActionListener( - evt -> { - if (evt.getSource().equals(refreshButton)) { - noteController.execute(null); - - } - } - ); - - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - - this.add(noteName); - this.add(noteInputField); - this.add(buttons); - } - - /** - * React to a button click that results in evt. - * @param evt the ActionEvent to react to - */ - public void actionPerformed(ActionEvent evt) { - System.out.println("Click " + evt.getActionCommand()); - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - final NoteState state = (NoteState) evt.getNewValue(); - setFields(state); - if (state.getError() != null) { - JOptionPane.showMessageDialog(this, state.getError(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } - - private void setFields(NoteState state) { - noteInputField.setText(state.getNote()); - } - - public void setNoteController(NoteController controller) { - this.noteController = controller; - } -} - diff --git a/src/main/java/view/NutritionAnalysisView.java b/src/main/java/view/NutritionAnalysisView.java new file mode 100644 index 000000000..3f50f6e62 --- /dev/null +++ b/src/main/java/view/NutritionAnalysisView.java @@ -0,0 +1,148 @@ +package view; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import interface_adapter.nutrition_analysis.NutritionAnalysisState; +import interface_adapter.nutrition_analysis.NutritionAnalysisViewModel; + +/** + * View for the nutrition analysis use case. + */ +public class NutritionAnalysisView extends JFrame { + + private final NutritionAnalysisViewModel viewModel; + private final JTextArea resultArea; + + public NutritionAnalysisView(NutritionAnalysisViewModel nutritionAnalysisViewModel) { + super("Nutrition Analysis result"); + + this.viewModel = nutritionAnalysisViewModel; + this.resultArea = new JTextArea(); + + initializeUi(); + observeViewModelState(); + } + + private void initializeUi() { + // Set size and default close operation for the JFrame + final int width = 450; + final int height = 650; + setSize(width, height); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + // Create and set up the panel with result area + final JPanel panel = createPanelWithResultArea(); + add(panel); + } + + private JPanel createPanelWithResultArea() { + final JPanel panel = new JPanel(new BorderLayout()); + + // Configure the result area (JTextArea) + resultArea.setEditable(false); + resultArea.setLineWrap(true); + resultArea.setWrapStyleWord(true); + + final JScrollPane scrollPane = new JScrollPane(resultArea); + panel.add(scrollPane, BorderLayout.CENTER); + return panel; + } + + private void observeViewModelState() { + // Observe changes to the "state" property of the view model + viewModel.addPropertyChangeListener(evt -> { + if ("state".equals(evt.getPropertyName())) { + updateResultArea((NutritionAnalysisState) evt.getNewValue()); + } + }); + } + + private void updateResultArea(NutritionAnalysisState state) { + if (state != null) { + final String displayedNutritionInfo = classifyNutrients(state.getNutritionResults()); + resultArea.setText(displayedNutritionInfo); + } + else { + resultArea.setText("State is Null"); + } + } + + /** + * The method handling the classification of all nutrient of the recipe. + * @param nutrientInfos a list of String containing all nutrients information of the recipe. + * @return a String containing all nutrients well classified. + */ + public static String classifyNutrients(List nutrientInfos) { + // Categorize nutrients + final Map> categorizedNutrients = new HashMap<>(); + categorizedNutrients.put("Macronutrients", new ArrayList<>()); + categorizedNutrients.put("Micronutrients", new ArrayList<>()); + categorizedNutrients.put("Vitamins", new ArrayList<>()); + categorizedNutrients.put("Minerals", new ArrayList<>()); + categorizedNutrients.put("Others", new ArrayList<>()); + + for (String info : nutrientInfos) { + categorizeNutrient(info, categorizedNutrients); + } + + // Build result string + return buildResultString(categorizedNutrients).trim(); + } + + private static void categorizeNutrient(String info, Map> categorizedNutrients) { + final String lowerInfo = info.trim().toLowerCase(); + + if (containsAny(lowerInfo, "carbohydrate", "protein", "fat", "energy", "fiber", "water")) { + categorizedNutrients.get("Macronutrients").add(info); + } + else if (lowerInfo.contains("vitamin")) { + if (containsAny(lowerInfo, "a", "b", "c", "d", "e", "k")) { + categorizedNutrients.get("Vitamins").add(info); + } + else { + categorizedNutrients.get("Micronutrients").add(info); + } + } + else if (containsAny(lowerInfo, "calcium", "iron", "magnesium", "phosphorus", "potassium", "sodium", "zinc")) { + categorizedNutrients.get("Minerals").add(info); + } + else { + categorizedNutrients.get("Others").add(info); + } + } + + private static boolean containsAny(String input, String... keywords) { + boolean contains = false; + for (String keyword : keywords) { + if (input.contains(keyword)) { + contains = true; + break; + } + } + return contains; + } + + private static String buildResultString(Map> categorizedNutrients) { + final StringBuilder result = new StringBuilder(); + + for (Map.Entry> entry : categorizedNutrients.entrySet()) { + if (!entry.getValue().isEmpty()) { + result.append(entry.getKey()).append(":\n"); + for (String info : entry.getValue()) { + result.append(" - ").append(info).append("\n"); + } + } + } + + return result.toString(); + } +} diff --git a/src/main/java/view/RecipeSearchView.java b/src/main/java/view/RecipeSearchView.java new file mode 100644 index 000000000..07b0b43d0 --- /dev/null +++ b/src/main/java/view/RecipeSearchView.java @@ -0,0 +1,672 @@ +package view; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.ListCellRenderer; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; + +import entity.Recipe; +import interface_adapter.nutrition_analysis.NutritionAnalysisController; +import interface_adapter.recipe_search.RecipeSearchController; +import interface_adapter.recipe_search.RecipeSearchPresenter; +import interface_adapter.recipe_search.RecipeSearchState; +import interface_adapter.recipe_search.RecipeSearchViewModel; +import interface_adapter.search_with_restriction.RestrictionController; +import interface_adapter.search_with_restriction.RestrictionState; +import interface_adapter.search_with_restriction.RestrictionViewModel; +import interface_adapter.serving_adjust.ServingAdjustController; +import interface_adapter.serving_adjust.ServingAdjustViewModel; + +/** + * View for recipe search. + */ +public class RecipeSearchView extends JPanel implements ActionListener, PropertyChangeListener { + + // Panels + private final JPanel inputPanel; + private final JPanel resultsPanel; + + // Text Fields + private final JTextField ingredientField; + + // Buttons + private final JButton addIngredientButton; + private final JButton removeIngredientButton; + private final JButton searchButton; + private final JButton addRestrictionButton; + private final JButton saveRecipeButton; + private final JButton analyzeNutritionButton; + private final JButton removeRestrictionsButton; + + // Lists + private final JList ingredientList; + private final JList recipeResults; + private final JList restrictionList; + + // Data Models + private final DefaultListModel ingredientListModel; + private final DefaultListModel recipeListModel; + private final DefaultListModel restrictionListModel; + + // Data Collections + private final List ingredients; + private final List restrictions; + private final Map> restrictionMap; + + // Controllers and ViewModels + private final RecipeSearchViewModel recipeSearchViewModel; + private final RestrictionViewModel restrictionViewModel; + private RecipeSearchController recipeSearchController; + private RestrictionController restrictionController; + private final ServingAdjustView servingAdjustView; + private ServingAdjustController servingAdjustController; + + // Optional reference to meal planning view for updates + private MealPlanningView mealPlanningView; + + // the nutrition analysis view and controller. + private NutritionAnalysisView nutritionAnalysisView; + private NutritionAnalysisController nutritionAnalysisController; + private ServingAdjustViewModel servingAdjustViewModel; + + /** + * View for recipe search. + * @param viewModel view model for recipe search. + * @param restrictionModel view model for restriction view model. + * @param controller controller for recipe search. + * @param restrictionController controller for restriction search. + * @param servingAdjustController controller for servingAdjust. + */ + public RecipeSearchView(RecipeSearchViewModel viewModel, RestrictionViewModel restrictionModel, + RecipeSearchController controller, RestrictionController restrictionController, + ServingAdjustController servingAdjustController) { + this.recipeSearchViewModel = viewModel; + this.recipeSearchViewModel.addPropertyChangeListener(this); + this.restrictionViewModel = restrictionModel; + this.restrictionViewModel.addPropertyChangeListener(this); + this.recipeSearchController = controller; + this.restrictionController = restrictionController; + this.servingAdjustController = servingAdjustController; + + // Initialize components + // UI Components - Labels + // UI Components + // Labels + final JLabel title = new JLabel(RecipeSearchViewModel.TITLE_LABEL); + + // Panels + inputPanel = new JPanel(); + final JPanel restrictionPanel = new JPanel(); + resultsPanel = new JPanel(); + + // Text Fields + ingredientField = new JTextField(ViewConstants.TEXTFIELD_WIDTH); + + // Buttons + addIngredientButton = new JButton(RecipeSearchViewModel.ADD_INGREDIENT_BUTTON_LABEL); + removeIngredientButton = new JButton(RecipeSearchViewModel.REMOVE_INGREDIENT_BUTTON_LABEL); + searchButton = new JButton(RecipeSearchViewModel.SEARCH_BUTTON_LABEL); + addRestrictionButton = new JButton(RestrictionViewModel.ADD_RESTRICTION_LABEL); + removeRestrictionsButton = new JButton(RestrictionViewModel.REMOVE_RESTRICTION_LABEL); + saveRecipeButton = new JButton(RecipeSearchViewModel.SAVE_RECIPE_LABEL); + analyzeNutritionButton = new JButton(RecipeSearchViewModel.ANALYZE_NUTRITION_LABEL); + + // Lists + ingredientListModel = new DefaultListModel<>(); + ingredientList = new JList<>(ingredientListModel); + restrictionListModel = new DefaultListModel<>(); + restrictionList = new JList<>(restrictionListModel); + recipeListModel = new DefaultListModel<>(); + recipeResults = new JList<>(recipeListModel); + + // Data Collections + ingredients = new ArrayList<>(); + restrictions = new ArrayList<>(); + restrictionMap = new HashMap<>(); + + // Controllers and ViewModels + servingAdjustView = new ServingAdjustView(evt -> handleServingAdjustment()); + + this.setLayout(new BorderLayout()); + + title.setFont(new Font(title.getFont().getName(), Font.BOLD, ViewConstants.TITLE_FONT_SIZE)); + title.setAlignmentX(Component.CENTER_ALIGNMENT); + + setupInputPanel(); + setupResultsPanel(); + + addIngredientButton.addActionListener(this); + removeIngredientButton.addActionListener(this); + searchButton.addActionListener(this); + addRestrictionButton.addActionListener(this); + saveRecipeButton.addActionListener(this); + analyzeNutritionButton.addActionListener(this); + removeRestrictionsButton.addActionListener(this); + + final JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + mainPanel.add(Box.createVerticalStrut(ViewConstants.VERTICAL_SPACING)); + mainPanel.add(title); + mainPanel.add(Box.createVerticalStrut(ViewConstants.VERTICAL_SPACING)); + mainPanel.add(inputPanel); + mainPanel.add(Box.createVerticalStrut(ViewConstants.VERTICAL_SPACING)); + mainPanel.add(restrictionPanel); + mainPanel.add(Box.createVerticalStrut(ViewConstants.VERTICAL_SPACING)); + searchButton.setAlignmentX(Component.CENTER_ALIGNMENT); + mainPanel.add(searchButton); + mainPanel.add(Box.createVerticalStrut(ViewConstants.VERTICAL_SPACING)); + mainPanel.add(resultsPanel); + + this.add(mainPanel, BorderLayout.CENTER); + } + + public Map> getRestrictionMap() { + return restrictionMap; + } + + public void setRecipeSearchController(RecipeSearchController controller) { + this.recipeSearchController = controller; + } + + public void setRestrictionController(RestrictionController restrictionController) { + this.restrictionController = restrictionController; + } + + // Method to set reference to meal planning view + public void setMealPlanningView(MealPlanningView mealPlanningView) { + this.mealPlanningView = mealPlanningView; + } + + public void setNutritionAnalysisController(NutritionAnalysisController nutritionAnalysisController) { + this.nutritionAnalysisController = nutritionAnalysisController; + } + + public void setNutritionAnalysisView(NutritionAnalysisView nutritionAnalysisView) { + this.nutritionAnalysisView = nutritionAnalysisView; + } + + public void setServingAdjustController(ServingAdjustController controller) { + this.servingAdjustController = controller; + } + + public void setServingAdjustViewModel(ServingAdjustViewModel viewModel) { + this.servingAdjustViewModel = viewModel; + } + + private void handleServingAdjustment() { + final int servings = servingAdjustView.getServings(); + + if (recipeSearchController != null && servingAdjustController != null) { + Object state = recipeSearchViewModel.getState(); + if (state == null) { + state = restrictionViewModel.getState(); + } + + if (state instanceof RecipeSearchState recipeState) { + final List recipes = recipeState.getRecipes(); + + servingAdjustController.updateServingsForAll(servings, recipes); + servingAdjustViewModel.updateRecipes(recipes); + + final RecipeSearchPresenter presenter = new RecipeSearchPresenter(recipeSearchViewModel); + presenter.presentRecipes(recipes); + + showMessage("All recipes updated to " + servings + " servings.", "Update Successful", + JOptionPane.INFORMATION_MESSAGE); + + } + else if (state instanceof RestrictionState restrictionState) { + final List recipes = restrictionState.getRecipes(); + + servingAdjustController.updateServingsForAll(servings, recipes); + servingAdjustViewModel.updateRecipes(recipes); + + final RecipeSearchPresenter presenter = new RecipeSearchPresenter(recipeSearchViewModel); + presenter.presentRecipes(recipes); + + showMessage("All restricted recipes updated to " + servings + " servings.", + "Update Successful", JOptionPane.INFORMATION_MESSAGE); + + } + else { + showMessage("Unknown state type for serving adjustment.", ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + } + else { + showMessage("Serving adjustment is not configured properly.", ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + } + + @Override + public void actionPerformed(ActionEvent evt) { + final Object source = evt.getSource(); + + if (source.equals(addIngredientButton)) { + handleAddIngredient(); + } + else if (source.equals(addRestrictionButton)) { + handleAddRestriction(); + } + else if (source.equals(saveRecipeButton)) { + handleSaveRecipe(); + } + else if (source.equals(removeIngredientButton)) { + handleRemoveIngredient(); + } + else if (source.equals(removeRestrictionsButton)) { + handleRemoveRestriction(); + } + else if (source.equals(searchButton)) { + handleSearch(); + } + else if (source.equals(analyzeNutritionButton)) { + handleAnalyzeNutrition(); + } + } + + // Extracted methods for each action + private void handleAddIngredient() { + final String ingredient = ingredientField.getText().trim(); + if (!ingredient.isEmpty()) { + ingredients.add(ingredient); + addValueToKey(restrictionMap, "Food Name", ingredient); + ingredientListModel.addElement(ingredient); + ingredientField.setText(""); + } + } + + private void handleAddRestriction() { + new FilterFrameView(this); + } + + private void handleSaveRecipe() { + final int selectedIndex = recipeResults.getSelectedIndex(); + if (selectedIndex == -1) { + showMessage("Please select a recipe to save first.", "No Recipe Selected", JOptionPane.WARNING_MESSAGE); + return; + } + + if (recipeSearchController != null) { + Object state = recipeSearchViewModel.getState(); + if (state == null) { + state = restrictionViewModel.getState(); + } + + if (state instanceof RecipeSearchState recipeState) { + final List recipes = recipeState.getRecipes(); + if (selectedIndex < recipes.size()) { + final Recipe selectedRecipe = recipes.get(selectedIndex); + recipeSearchController.saveRecipe(getCurrentUserId(), selectedRecipe); + + if (mealPlanningView != null && mealPlanningView.getController() != null) { + mealPlanningView.refreshSavedRecipes(); + } + } + } + else if (state instanceof RestrictionState restrictionState) { + final List recipes = restrictionState.getRecipes(); + if (selectedIndex < recipes.size()) { + final Recipe selectedRecipe = recipes.get(selectedIndex); + recipeSearchController.saveRecipe(getCurrentUserId(), selectedRecipe); + + if (mealPlanningView != null && mealPlanningView.getController() != null) { + mealPlanningView.refreshSavedRecipes(); + } + } + } + else { + showMessage("Unknown state type.", ViewConstants.ERROR_MESSAGE, JOptionPane.ERROR_MESSAGE); + } + } + } + + private void handleRemoveIngredient() { + final int selectedIndex = ingredientList.getSelectedIndex(); + if (selectedIndex != -1) { + ingredients.remove(selectedIndex); + ingredientListModel.remove(selectedIndex); + } + } + + private void handleRemoveRestriction() { + final int selectedIndex = restrictionList.getSelectedIndex(); + if (selectedIndex != -1) { + restrictions.remove(selectedIndex); + restrictionListModel.remove(selectedIndex); + removeByJList(restrictionList.getModel().getElementAt(selectedIndex)); + } + else { + showMessage("Please select restrictions to remove.", "No Restrictions Selected", + JOptionPane.WARNING_MESSAGE); + } + } + + private void handleSearch() { + if (recipeSearchController != null && restrictionController != null) { + if (restrictions.isEmpty()) { + recipeSearchController.executeSearch(new ArrayList<>(ingredients)); + } + else { + restrictionController.executeRestrictionSearch(restrictionMap); + } + } + } + + private void handleAnalyzeNutrition() { + final int selectedIndex = recipeResults.getSelectedIndex(); + if (selectedIndex == -1) { + showMessage("Please select a recipe to analyze first.", "No Recipe Selected", + JOptionPane.WARNING_MESSAGE); + } + else if (nutritionAnalysisController != null) { + Object state = recipeSearchViewModel.getState(); + + if (state == null) { + state = restrictionViewModel.getState(); + } + + if (state instanceof RecipeSearchState recipeState) { + final List recipes = recipeState.getRecipes(); + if (selectedIndex < recipes.size()) { + final Recipe selectedRecipe = recipes.get(selectedIndex); + nutritionAnalysisController.executeAnalysis(selectedRecipe); + nutritionAnalysisView.setVisible(true); + } + else { + showMessage("Selected recipe index is out of bounds.", ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + } + else if (state instanceof RestrictionState restrictionState) { + final List recipes = restrictionState.getRecipes(); + if (selectedIndex < recipes.size()) { + final Recipe selectedRecipe = recipes.get(selectedIndex); + nutritionAnalysisController.executeAnalysis(selectedRecipe); + nutritionAnalysisView.setVisible(true); + } + else { + showMessage("Selected recipe index is out of bounds.", ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + } + else { + showMessage("Unknown state type for nutrition analysis.", ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + } + else { + showMessage("Nutrition analysis is not configured properly.", ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + } + // Utility method for showing messages + private void showMessage(String message, String title, int messageType) { + JOptionPane.showMessageDialog(this, message, title, messageType); + } + + // Helper method for remove restrictions + private void removeByJList(String valueToRemove) { + final List valuesToRemove = Arrays.asList(valueToRemove.split(" ")); + + restrictionMap.values().removeIf(valuesToRemove::contains); + } + + // For testing purposes + private int getCurrentUserId() { + return 1; + } + + private void setupInputPanel() { + inputPanel.setLayout(new BoxLayout(inputPanel, BoxLayout.Y_AXIS)); + + final JPanel addIngredientPanel = createAddIngredientPanel(); + final JPanel labelPanel = createLabelPanel(); + final JPanel buttonPanel = createButtonPanel(); + final JPanel displayPanel = createDisplayPanel(); + + inputPanel.add(addIngredientPanel); + inputPanel.add(labelPanel); + inputPanel.add(Box.createVerticalStrut(ViewConstants.VERTICAL_SPACING)); + inputPanel.add(displayPanel); + inputPanel.add(buttonPanel); + } + + // Extracted helper methods + private JPanel createAddIngredientPanel() { + final JPanel addIngredientPanel = new JPanel(); + addIngredientPanel.setLayout(new BoxLayout(addIngredientPanel, BoxLayout.Y_AXIS)); + addIngredientPanel.add(new JLabel(RecipeSearchViewModel.ENTER_INGREDIENT_LABEL)); + addIngredientPanel.add(ingredientField); + return addIngredientPanel; + } + + private JPanel createLabelPanel() { + final JPanel labelPanel = new JPanel(); + labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.X_AXIS)); + + final JLabel ingredientLabel = new JLabel(RecipeSearchViewModel.INGREDIENT_TITLE_LABEL); + ingredientLabel.setHorizontalAlignment(SwingConstants.LEFT); + + final JPanel restrictionLabelPanel = new JPanel(); + restrictionLabelPanel.add(new JLabel(RestrictionViewModel.RESTRICTION_TITLE_LABEL)); + + labelPanel.add(ingredientLabel); + labelPanel.add(restrictionLabelPanel); + + return labelPanel; + } + + private JPanel createButtonPanel() { + final JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); + + buttonPanel.add(addIngredientButton); + buttonPanel.add(Box.createHorizontalStrut(ViewConstants.HORIZONTAL_STRUT_SMALL)); + buttonPanel.add(removeIngredientButton); + buttonPanel.add(Box.createHorizontalStrut(ViewConstants.HORIZONTAL_STRUT_LARGE)); + buttonPanel.add(addRestrictionButton); + buttonPanel.add(Box.createHorizontalStrut(ViewConstants.HORIZONTAL_STRUT_SMALL)); + buttonPanel.add(removeRestrictionsButton); + + return buttonPanel; + } + + private JPanel createDisplayPanel() { + final JPanel displayPanel = new JPanel(); + displayPanel.setLayout(new BoxLayout(displayPanel, BoxLayout.X_AXIS)); + + final JScrollPane ingredientScrollPane = new JScrollPane(ingredientList); + ingredientScrollPane.setPreferredSize( + new Dimension(ViewConstants.INGREDIENT_LIST_WIDTH, ViewConstants.INGREDIENT_LIST_HEIGHT)); + + final JScrollPane restrictionScrollPane = new JScrollPane(restrictionList); + restrictionScrollPane.setPreferredSize( + new Dimension(ViewConstants.INGREDIENT_LIST_WIDTH, ViewConstants.INGREDIENT_LIST_HEIGHT)); + + displayPanel.add(ingredientScrollPane); + displayPanel.add(restrictionScrollPane); + + return displayPanel; + } + + private void setupResultsPanel() { + resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.Y_AXIS)); + resultsPanel.setBorder(BorderFactory.createEmptyBorder(ViewConstants.VERTICAL_PADDING, + ViewConstants.VERTICAL_PADDING, ViewConstants.VERTICAL_PADDING, ViewConstants.VERTICAL_PADDING)); + + resultsPanel.add(createResultsLabel()); + resultsPanel.add(Box.createVerticalStrut(ViewConstants.VERTICAL_SPACING)); + resultsPanel.add(createResultsScrollPane()); + resultsPanel.add(createBottomButtonPanel()); + } + + private JLabel createResultsLabel() { + final JLabel resultsLabel = new JLabel(RecipeSearchViewModel.RECIPE_RESULT_LABEL); + resultsLabel.setFont(new Font("Arial", Font.BOLD, ViewConstants.RESULTS_LABEL_FONT_SIZE)); + resultsLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + return resultsLabel; + } + + private JScrollPane createResultsScrollPane() { + recipeResults.setFont(new Font("Monospaced", Font.PLAIN, ViewConstants.RESULTS_LIST_FONT_SIZE)); + recipeResults.setBorder(BorderFactory.createEmptyBorder(ViewConstants.SEARCHING_BORDER_PADDING, + ViewConstants.SEARCHING_BORDER_PADDING, ViewConstants.SEARCHING_BORDER_PADDING, + ViewConstants.SEARCHING_BORDER_PADDING)); + recipeResults.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + recipeResults.setCellRenderer(createCustomCellRenderer()); + + final JScrollPane resultsScrollPane = new JScrollPane(recipeResults); + resultsScrollPane.setPreferredSize(new Dimension(ViewConstants.RESULTS_LIST_WIDTH, + ViewConstants.RESULTS_LIST_HEIGHT)); + resultsScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + resultsScrollPane.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(ViewConstants.SEARCHING_BORDER_PADDING, + ViewConstants.SEARCHING_BORDER_PADDING, ViewConstants.SEARCHING_BORDER_PADDING, + ViewConstants.SEARCHING_BORDER_PADDING), + BorderFactory.createLineBorder(Color.GRAY) + )); + return resultsScrollPane; + } + + private ListCellRenderer createCustomCellRenderer() { + return new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + final JLabel label = (JLabel) super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus); + + if (isSelected) { + label.setBackground(ViewConstants.SELECTED_BACKGROUND_COLOR); + label.setForeground(Color.BLACK); + } + else { + label.setBackground(ViewConstants.DEFAULT_BACKGROUND_COLOR); + } + label.setBorder(BorderFactory.createEmptyBorder(ViewConstants.SEARCHING_BORDER_PADDING, + ViewConstants.VERTICAL_PADDING, + ViewConstants.SEARCHING_BORDER_PADDING, ViewConstants.VERTICAL_PADDING)); + return label; + } + }; + } + + private JPanel createBottomButtonPanel() { + final JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); + buttonPanel.setBorder(BorderFactory.createEmptyBorder(ViewConstants.VERTICAL_PADDING, 0, + ViewConstants.VERTICAL_PADDING, 0)); + + configureButton(analyzeNutritionButton); + configureButton(saveRecipeButton); + + buttonPanel.add(Box.createHorizontalGlue()); + buttonPanel.add(analyzeNutritionButton); + buttonPanel.add(Box.createHorizontalStrut(ViewConstants.VERTICAL_PADDING)); + buttonPanel.add(saveRecipeButton); + buttonPanel.add(Box.createHorizontalStrut(ViewConstants.VERTICAL_PADDING)); + buttonPanel.add(servingAdjustView); + buttonPanel.add(Box.createHorizontalGlue()); + + return buttonPanel; + } + + private void configureButton(JButton button) { + button.setBackground(ViewConstants.BUTTON_BACKGROUND_COLOR); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + final Object newState = evt.getNewValue(); + + if (newState instanceof RecipeSearchState recipeState) { + + if (recipeState.getError() != null) { + JOptionPane.showMessageDialog(this, + recipeState.getError(), + ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + else if (recipeState.getMessage() != null) { + JOptionPane.showMessageDialog(this, + recipeState.getMessage(), + "Message", + JOptionPane.INFORMATION_MESSAGE); + } + else { + updateRecipeResults(recipeState.getRecipeResults()); + } + + } + else if (newState instanceof RestrictionState restrictionState) { + + if (restrictionState.getError() != null) { + JOptionPane.showMessageDialog(this, + restrictionState.getError(), + ViewConstants.ERROR_MESSAGE, + JOptionPane.ERROR_MESSAGE); + } + else if (restrictionState.getMessage() != null) { + JOptionPane.showMessageDialog(this, + restrictionState.getMessage(), + "Message", + JOptionPane.INFORMATION_MESSAGE); + } + else { + updateRecipeResults(restrictionState.getRecipeResults()); + } + + } + else { + System.out.println("Unexpected state type: " + newState.getClass().getName()); + } + } + + /** + * Update selection for restriction search function. + * @param selectionText the text selected in filter frame view. + */ + public void updateSelection(String selectionText) { + restrictionListModel.addElement(selectionText); + restrictions.add(selectionText); + } + + private void updateRecipeResults(List recipes) { + recipeListModel.clear(); + for (String recipe : recipes) { + recipeListModel.addElement(recipe); + } + } + + private static void addValueToKey(Map> map, String key, String value) { + map.computeIfAbsent(key, k -> new ArrayList<>()).add(value); + } +} diff --git a/src/main/java/view/SavedRecipesPanel.java b/src/main/java/view/SavedRecipesPanel.java new file mode 100644 index 000000000..5d165986a --- /dev/null +++ b/src/main/java/view/SavedRecipesPanel.java @@ -0,0 +1,99 @@ +package view; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.util.List; + +// Swing imports +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; + +// Application imports +import entity.Recipe; +import interface_adapter.meal_planning.MealPlanningController; + +/** + * Panel for displaying and managing saved recipes. + * Allows selection of recipes and meal types for meal planning. + */ + +class SavedRecipesPanel extends JPanel { + private final DefaultListModel recipesModel; + private final JList recipesList; + private final JComboBox mealTypeCombo; + private MealPlanningController controller; + + SavedRecipesPanel() { + super(new BorderLayout(ViewConstants.HORIZONTAL_GAP, ViewConstants.VERTICAL_GAP)); + setBorder(BorderFactory.createTitledBorder("Saved Recipes")); + setPreferredSize(new Dimension(ViewConstants.PANEL_WIDTH, 0)); + + recipesModel = new DefaultListModel<>(); + recipesList = new JList<>(recipesModel); + recipesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + mealTypeCombo = new JComboBox<>(ViewConstants.MEAL_TYPES); + + add(createControlsPanel(), BorderLayout.NORTH); + add(new JScrollPane(recipesList), BorderLayout.CENTER); + } + + private JPanel createControlsPanel() { + final JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.add(new JLabel("Meal Type:")); + panel.add(mealTypeCombo); + return panel; + } + + void updateRecipesList(List recipes) { + recipesModel.clear(); + if (recipes != null) { + recipes.forEach(recipesModel::addElement); + } + } + + Recipe getSelectedRecipe() { + return recipesList.getSelectedValue(); + } + + String getSelectedMealType() { + return (String) mealTypeCombo.getSelectedItem(); + } + + void setController(MealPlanningController controller) { + this.controller = controller; + recipesList.setCellRenderer(new RecipeListCellRenderer()); + } + + /** + * Fromats saved recipes. + * @return The rendered component + */ + private final class RecipeListCellRenderer extends DefaultListCellRenderer { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof Recipe recipe) { + setText(recipe.getTitle()); + setToolTipText(createTooltip(recipe)); + } + return this; + } + + private String createTooltip(Recipe recipe) { + return String.format(recipe.getTitle()); + } + } +} diff --git a/src/main/java/view/ServingAdjustView.java b/src/main/java/view/ServingAdjustView.java new file mode 100644 index 000000000..d3a3fe0b5 --- /dev/null +++ b/src/main/java/view/ServingAdjustView.java @@ -0,0 +1,80 @@ +package view; + +import java.awt.Dimension; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +/** + * View component for adjusting the number of servings. + */ +public class ServingAdjustView extends JPanel { + public static final int FIFTY = 50; + public static final int FIVE = 5; + public static final int TEN = 10; + public static final int TWENTY_FIVE = 25; + private static final int DEFAULT_SERVINGS = 1; + + private final JTextField servingInputField; + private final JButton updateButton; + + /** + * Constructs a ServingAdjustView with the specified action listener for the update button. + * + * @param updateAction The action listener to be invoked when the update button is pressed. + */ + public ServingAdjustView(ActionListener updateAction) { + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + setBorder(BorderFactory.createEmptyBorder(TEN, TEN, TEN, TEN)); + + add(new JLabel("Servings:")); + servingInputField = new JTextField(String.valueOf(DEFAULT_SERVINGS), FIVE); + servingInputField.setMaximumSize(new Dimension(FIFTY, TWENTY_FIVE)); + add(servingInputField); + + updateButton = new JButton("Update"); + updateButton.addActionListener(updateAction); + add(Box.createHorizontalStrut(TEN)); + add(updateButton); + } + + /** + * Retrieves the servings value entered by the user. + * + * @return The servings value as an integer. Defaults to 1 if the input is invalid. + */ + public int getServings() { + int servings = DEFAULT_SERVINGS; + try { + final int inputServings = Integer.parseInt(servingInputField.getText().trim()); + if (inputServings > 0) { + servings = inputServings; + } + else { + throw new NumberFormatException("Servings must be a positive number."); + } + } + catch (NumberFormatException ex) { + JOptionPane.showMessageDialog(this, "Please enter a valid positive number for servings.", + "Invalid Input", JOptionPane.ERROR_MESSAGE); + servingInputField.setText(String.valueOf(DEFAULT_SERVINGS)); + } + return servings; + } + + /** + * Sets the servings value in the input field. + * + * @param servings The servings value to display in the input field. + */ + public void setServings(int servings) { + servingInputField.setText(String.valueOf(servings)); + } +} diff --git a/src/main/java/view/ViewConstants.java b/src/main/java/view/ViewConstants.java new file mode 100644 index 000000000..9b145aff7 --- /dev/null +++ b/src/main/java/view/ViewConstants.java @@ -0,0 +1,50 @@ +package view; + +import java.awt.Color; + +/** + * Constants used throughout the view layer. + * Contains layout measurements, colors, and standard options. + */ + +public final class ViewConstants { + + // View constants for meal planning + public static final int VERTICAL_GAP = 10; + public static final int HORIZONTAL_GAP = 10; + public static final int PLANNING_BORDER_PADDING = 10; + public static final int TITLE_SIZE = 16; + public static final int PANEL_WIDTH = 250; + + public static final int DAYS_IN_WEEK = 7; + public static final int BUTTON_WIDTH = 40; + public static final int BUTTON_HEIGHT = 25; + public static final Color COMPLETED_COLOR = new Color(200, 255, 200); + public static final Color IN_PROGRESS_COLOR = new Color(255, 255, 200); + public static final Color DEFAULT_COLOR = Color.WHITE; + + public static final String[] MEAL_TYPES = {"Breakfast", "Lunch", "Dinner", "Snack"}; + public static final String[] MEAL_STATUSES = {"Planned", "In Progress", "Completed"}; + + // View constants for recipe searching + public static final int TEXTFIELD_WIDTH = 20; + public static final int VERTICAL_SPACING = 10; + public static final int INGREDIENT_LIST_HEIGHT = 100; + public static final int INGREDIENT_LIST_WIDTH = 300; + public static final int RESULTS_LIST_HEIGHT = 200; + public static final int RESULTS_LIST_WIDTH = 300; + public static final int TITLE_FONT_SIZE = 16; + public static final int HORIZONTAL_STRUT_SMALL = 30; + public static final int HORIZONTAL_STRUT_LARGE = 80; + public static final int VERTICAL_PADDING = 10; + public static final int SEARCHING_BORDER_PADDING = 5; + public static final int RESULTS_LABEL_FONT_SIZE = 14; + public static final int RESULTS_LIST_FONT_SIZE = 12; + public static final Color SELECTED_BACKGROUND_COLOR = new Color(200, 220, 240); + public static final Color DEFAULT_BACKGROUND_COLOR = Color.WHITE; + public static final Color BUTTON_BACKGROUND_COLOR = new Color(240, 240, 240); + public static final String ERROR_MESSAGE = "Error"; + + private ViewConstants() { + } +} diff --git a/src/main/java/view/ViewUtils.java b/src/main/java/view/ViewUtils.java new file mode 100644 index 000000000..fc81c09bf --- /dev/null +++ b/src/main/java/view/ViewUtils.java @@ -0,0 +1,70 @@ +package view; + +import java.awt.Component; +import java.awt.Font; +import java.time.LocalDate; + +// Swing imports +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.SwingConstants; + +// Application imports +import interface_adapter.meal_planning.MealPlanningState; + +/** + * Utility methods for the view. + */ +final class ViewUtils { + private ViewUtils() { + + } + + static LocalDate getCurrentWeekStart() { + final LocalDate today = LocalDate.now(); + return today.minusDays(today.getDayOfWeek().getValue() - 1); + } + + static int getCurrentUserId() { + // Temporary implementation + return 1; + } + + static JLabel createTitleLabel(String text) { + final JLabel label = new JLabel(text); + label.setFont(new Font(label.getFont().getName(), Font.BOLD, ViewConstants.TITLE_SIZE)); + label.setHorizontalAlignment(SwingConstants.CENTER); + return label; + } + + static void showErrorDialog(Component parent, String message, Exception exception) { + JOptionPane.showMessageDialog(parent, + message + ": " + exception.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + } + + static void handleStateChange(Component parent, MealPlanningState state) { + if (state.getError() != null) { + showErrorDialog(parent, state.getError(), new IllegalStateException(state.getError())); + state.setError(null); + } + + if (state.getMessage() != null) { + JOptionPane.showMessageDialog(parent, + state.getMessage(), + "Success", + JOptionPane.INFORMATION_MESSAGE); + state.setMessage(null); + } + } + + public static String capitalizeFirstLetter(String input) { + String result = input; + if (input != null && !input.isEmpty()) { + result = input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase(); + } + return result; + } +} + diff --git a/src/main/java/view/WeekNavigationPanel.java b/src/main/java/view/WeekNavigationPanel.java new file mode 100644 index 000000000..8c641ca28 --- /dev/null +++ b/src/main/java/view/WeekNavigationPanel.java @@ -0,0 +1,43 @@ +package view; + +import java.awt.FlowLayout; +import java.awt.event.ActionListener; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import interface_adapter.meal_planning.MealPlanningState; + +/** + * Panel for week navigation. + */ +class WeekNavigationPanel extends JPanel { + private final JLabel weekLabel; + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM d, yyyy"); + + WeekNavigationPanel(ActionListener listener) { + super(new FlowLayout(FlowLayout.CENTER)); + + final JButton prevButton = new JButton("Previous Week"); + final JButton nextButton = new JButton("Next Week"); + weekLabel = new JLabel(); + + prevButton.addActionListener(listener); + nextButton.addActionListener(listener); + + add(prevButton); + add(weekLabel); + add(nextButton); + } + + void updateWeekLabel(MealPlanningState state) { + if (state != null) { + final LocalDate start = state.getCurrentWeekStart(); + final LocalDate end = start.plusDays(6); + weekLabel.setText(String.format("%s - %s", start.format(formatter), end.format(formatter))); + } + } +} diff --git a/src/test/java/app/MainNoteApplicationTest.java b/src/test/java/app/MainNoteApplicationTest.java deleted file mode 100644 index 025d970e2..000000000 --- a/src/test/java/app/MainNoteApplicationTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package app; - -import entity.User; -import org.junit.Before; -import org.junit.Test; -import use_case.note.NoteDataAccessInterface; - -import javax.swing.*; -import java.awt.*; - -import static java.lang.Thread.sleep; -import static org.junit.Assert.*; - -public class MainNoteApplicationTest { - - private JFrame app; - - @Before - public void setUp() { - - // create the data access and inject it into our builder! - final NoteDataAccessInterface noteDataAccess = new NoteDataAccessInterface() { - - private String note = "test"; - - @Override - public String saveNote(User user, String note) { - this.note = note; - return note; - } - - @Override - public String loadNote(User user) { - return note; - } - }; - - final NoteAppBuilder builder = new NoteAppBuilder(); - app = builder.addNoteDAO(noteDataAccess) - .addNoteView() - .addNoteUseCase().build(); - - app.setVisible(true); - - } - - /** - * This is an example of an end-to-end test with a mocked database. - *

The code creates the application and directly tests to see that the GUI - * is updated as expected when the buttons and UI elements are interacted with. - *

- * You can run the test to visually see what happens. - */ - @Test - public void testEndToEnd() { - - Component[] components = ((JPanel)app.getRootPane().getContentPane().getComponents()[0]).getComponents(); - JTextArea textArea = null; - for (Component component : components) { - if (component instanceof JTextArea) { - textArea = (JTextArea) component; - assertEquals("test", textArea.getText()); - - } - } - - textArea.setText("test test"); - - - JButton save = null; - JButton load = null; - for (Component component : components) { - if (component instanceof JPanel) { - for (Component c : ((JPanel) component).getComponents()) { - if (c instanceof JButton) { - if (save != null) { - load = (JButton) c; - } - else { - save = (JButton) c; - } - } - } - } - } - - save.doClick(); - assertEquals("test test", textArea.getText()); - textArea.setText(""); - - System.out.println("cleared text; about to refresh..."); - // pause execution for a bit so we can visually see the changes on the screen - try { - sleep(1500); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - load.doClick(); - assertEquals("test test", textArea.getText()); - - System.out.println("after refresh!"); - - // pause execution for a bit so we can visually see the changes on the screen - try { - sleep(1500); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - } -} \ No newline at end of file diff --git a/src/test/java/entityTest/FoodTest.java b/src/test/java/entityTest/FoodTest.java new file mode 100644 index 000000000..1b8bd7d8a --- /dev/null +++ b/src/test/java/entityTest/FoodTest.java @@ -0,0 +1,96 @@ +package entityTest; + +import entity.Food; +import entity.Nutrition; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class FoodTest { + + @Test + public void testFoodInitialization() { + // Use a simple Nutrition object + Nutrition nutrition = new Nutrition(250.0, 10.0, 8.0, 10.0, 10.0, 10.0); + + // Create a Food object + Food food = new Food(1, "Apple", "Fruit", "grams", 100.0, nutrition); + + // Assert Food attributes + assertEquals(1, food.getId()); + assertEquals("Apple", food.getName()); + assertEquals("Fruit", food.getCategory()); + assertEquals("grams", food.getUnit()); + assertEquals(100.0, food.getServingSize(), 0.0); // Use delta for double comparison + + // Assert that the Nutrition object is correctly stored + assertSame(nutrition, food.getNutrition()); + } + + @Test + public void testSettersAndGetters() { + // Use a simple Nutrition object + Nutrition initialNutrition = new Nutrition(300.0, 20.0, 15.0, 10.0, 10.0, 10.0); + + // Create a Food object + Food food = new Food(1, "Banana", "Fruit", "grams", 150.0, initialNutrition); + + // Update Food attributes + food.setId(2); + food.setName("Orange"); + food.setCategory("Citrus"); + food.setUnit("pieces"); + food.setServingSize(200.0); + + // Update Nutrition object + Nutrition updatedNutrition = new Nutrition(400.0, 25.0, 20.0, 10.0, 10.0, 10.0); + food.setNutrition(updatedNutrition); + + // Assert updated Food attributes + assertEquals(2, food.getId()); + assertEquals("Orange", food.getName()); + assertEquals("Citrus", food.getCategory()); + assertEquals("pieces", food.getUnit()); + assertEquals(200.0, food.getServingSize(), 0.0); // Use delta for double comparison + + // Assert updated Nutrition object + assertSame(updatedNutrition, food.getNutrition()); + } + + @Test + public void testNullValues() { + // Create a Food object with null values for optional attributes + Food food = new Food(3, null, null, null, 50.0, null); + + // Assert null attributes + assertNull(food.getName()); + assertNull(food.getCategory()); + assertNull(food.getUnit()); + assertNull(food.getNutrition()); + + // Assert non-null attributes + assertEquals(3, food.getId()); + assertEquals(50.0, food.getServingSize(), 0.0); // Use delta for double comparison + } + + @Test + public void testNegativeServingSize() { + // Test Food with a negative serving size + Food food = new Food(4, "Ice Cream", "Dessert", "grams", -150.0, null); + + // Assert negative serving size (if not validated) + assertEquals(-150.0, food.getServingSize(), 0.0); // Use delta for double comparison + } + + @Test + public void testSetInvalidServingSize() { + // Create a Food object + Food food = new Food(5, "Yogurt", "Dairy", "grams", 200.0, null); + + // Update to an invalid serving size + food.setServingSize(-50.0); + + // Assert that the serving size was updated (assuming no validation logic exists) + assertEquals(-50.0, food.getServingSize(), 0.0); // Use delta for double comparison + } +} \ No newline at end of file diff --git a/src/test/java/entityTest/IngredientTest.java b/src/test/java/entityTest/IngredientTest.java new file mode 100644 index 000000000..19b49508b --- /dev/null +++ b/src/test/java/entityTest/IngredientTest.java @@ -0,0 +1,130 @@ +package entityTest; + +import entity.Ingredient; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class IngredientTest { + + private Ingredient ingredient; + + // Set up a default Ingredient object before each test + @Before + public void setUp() { + ingredient = new Ingredient(1, "Sugar", 100.0, "grams"); + } + + @Test + public void testConstructor_validInput() { + // Check that the object is created successfully with valid parameters + assertNotNull(ingredient); + assertEquals(1, ingredient.getIngredientId()); + assertEquals("Sugar", ingredient.getName()); + assertEquals(100.0, ingredient.getQuantity(), 0.01); + assertEquals("grams", ingredient.getUnit()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_invalidQuantity() { + // Test invalid quantity (negative) + new Ingredient(2, "Salt", -50.0, "grams"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_nullName() { + // Test null name + new Ingredient(3, null, 100.0, "grams"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_emptyName() { + // Test empty name + new Ingredient(4, "", 100.0, "grams"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_nullUnit() { + // Test null unit + new Ingredient(5, "Flour", 100.0, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_emptyUnit() { + // Test empty unit + new Ingredient(6, "Flour", 100.0, ""); + } + + @Test + public void testGetIngredientId() { + assertEquals(1, ingredient.getIngredientId()); + } + + @Test + public void testGetName() { + assertEquals("Sugar", ingredient.getName()); + } + + @Test + public void testGetQuantity() { + assertEquals(100.0, ingredient.getQuantity(), 0.01); + } + + @Test + public void testGetUnit() { + assertEquals("grams", ingredient.getUnit()); + } + + @Test + public void testSetQuantity() { + // Test setting the quantity to a new value + ingredient.setQuantity(200.0); + assertEquals(200.0, ingredient.getQuantity(), 0.01); + } + + @Test(expected = IllegalArgumentException.class) + public void testScaleQuantity_invalidFactor() { + // Test scaling with a negative factor + ingredient.scaleQuantity(-2.0); + } + + @Test + public void testScaleQuantity_validFactor() { + // Test scaling with a valid factor + Ingredient scaledIngredient = ingredient.scaleQuantity(2.0); + assertEquals(200.0, scaledIngredient.getQuantity(), 0.01); + } + + @Test + public void testScaleQuantity_noChange() { + // Test scaling with a factor of 1.0 (no change) + Ingredient scaledIngredient = ingredient.scaleQuantity(1.0); + assertEquals(100.0, scaledIngredient.getQuantity(), 0.01); + } + + @Test + public void testScaleQuantity_sameIngredient() { + // Ensure that the scaled ingredient is a new object, not the same instance + Ingredient scaledIngredient = ingredient.scaleQuantity(2.0); + assertNotSame(ingredient, scaledIngredient); // Verifying they are different objects + } + + @Test + public void testScaleQuantity_zeroFactor() { + Ingredient scaledIngredient = ingredient.scaleQuantity(0.0); + assertEquals(0.0, scaledIngredient.getQuantity(), 0.01); + } + + @Test + public void testLargeQuantity() { + Ingredient largeIngredient = new Ingredient(1000000, "Large Ingredient", 1e6, "grams"); + assertEquals(1e6, largeIngredient.getQuantity(), 0.01); + } + + @Test + public void testScaleQuantity_largeFactor() { + Ingredient scaledIngredient = ingredient.scaleQuantity(1e6); + assertEquals(100.0 * 1e6, scaledIngredient.getQuantity(), 0.01); + } +} + diff --git a/src/test/java/entityTest/MealPlanEntryTest.java b/src/test/java/entityTest/MealPlanEntryTest.java new file mode 100644 index 000000000..1ddc62211 --- /dev/null +++ b/src/test/java/entityTest/MealPlanEntryTest.java @@ -0,0 +1,140 @@ +package entityTest; + +import entity.MealPlanEntry; +import entity.Recipe; +import entity.Ingredient; +import entity.Food; +import entity.Nutrition; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.json.JSONArray; + +public class MealPlanEntryTest { + + private MealPlanEntry mealPlanEntry; + private Recipe recipe; + + @Before + public void setUp() { + // Create mock or basic instances for the complex attributes of Recipe + + // Mock or create Ingredients (just basic test data for demonstration) + Ingredient ingredient1 = new Ingredient(1, "Flour", 500, "grams"); + Ingredient ingredient2 = new Ingredient(2, "Sugar", 200, "grams"); + List ingredients = new ArrayList<>(); + ingredients.add(ingredient1); + ingredients.add(ingredient2); + + // Create Nutrition object (assuming it's another class with minimal attributes) + Nutrition nutrition = new Nutrition(100.0, 5.0, 2.0, 30.0, 10.0, 15.0); // Mock values for the example + + // Create Food objects (mocking them with minimal attributes) + Food food1 = new Food(1, "Pizza", "Main Dish", "slice", 1.0, nutrition); + Food food2 = new Food(2, "Pasta", "Main Dish", "bowl", 1.5, nutrition); + List foodList = new ArrayList<>(); + foodList.add(food1); + foodList.add(food2); + + // Create a JSONArray for jsonIngredient (assuming you need some mock data) + JSONArray jsonIngredients = new JSONArray(); + jsonIngredients.put("ingredient1"); + jsonIngredients.put("ingredient2"); + + // Create a Recipe instance with the mocked/constructed data + recipe = new Recipe(1, "Spaghetti Bolognese", "A classic Italian pasta dish.", ingredients, "Cook spaghetti, then add sauce.", nutrition, foodList, jsonIngredients, 4); + + // Now create the MealPlanEntry with the mock Recipe + mealPlanEntry = new MealPlanEntry(1, recipe, LocalDate.of(2024, 11, 26), 101, "Dinner"); + } + + @Test + public void testConstructor_validInput() { + // Check that the object is created correctly with valid parameters + assertNotNull(mealPlanEntry); + assertEquals(1, mealPlanEntry.getEntryId()); + assertEquals(recipe, mealPlanEntry.getRecipe()); + assertEquals(LocalDate.of(2024, 11, 26), mealPlanEntry.getDate()); + assertEquals(101, mealPlanEntry.getUserId()); + assertEquals("Dinner", mealPlanEntry.getMealType()); + assertEquals("planned", mealPlanEntry.getStatus()); // Default status should be "planned" + } + + @Test + public void testGetEntryId() { + assertEquals(1, mealPlanEntry.getEntryId()); + } + + @Test + public void testGetRecipe() { + assertEquals(recipe, mealPlanEntry.getRecipe()); + } + + @Test + public void testGetDate() { + assertEquals(LocalDate.of(2024, 11, 26), mealPlanEntry.getDate()); + } + + @Test + public void testGetUserId() { + assertEquals(101, mealPlanEntry.getUserId()); + } + + @Test + public void testGetMealType() { + assertEquals("Dinner", mealPlanEntry.getMealType()); + } + + @Test + public void testGetStatus() { + assertEquals("planned", mealPlanEntry.getStatus()); + } + + @Test + public void testSetEntryId() { + mealPlanEntry.setEntryId(2); + assertEquals(2, mealPlanEntry.getEntryId()); + } + + @Test + public void testSetRecipe() { + // Create a new Recipe to replace the old one + Recipe newRecipe = new Recipe(2, "Chicken Salad", "A fresh chicken salad.", new ArrayList<>(), "Mix ingredients", new Nutrition(100.0, 5.0, 2.0, 30.0, 10.0, 15.0), new ArrayList<>(), new JSONArray(), 2); + mealPlanEntry.setRecipe(newRecipe); + assertEquals(newRecipe, mealPlanEntry.getRecipe()); + } + + @Test + public void testSetDate() { + mealPlanEntry.setDate(LocalDate.of(2025, 12, 31)); + assertEquals(LocalDate.of(2025, 12, 31), mealPlanEntry.getDate()); + } + + @Test + public void testSetUserId() { + mealPlanEntry.setUserId(102); + assertEquals(102, mealPlanEntry.getUserId()); + } + + @Test + public void testSetMealType() { + mealPlanEntry.setMealType("Lunch"); + assertEquals("Lunch", mealPlanEntry.getMealType()); + } + + @Test + public void testSetStatus() { + mealPlanEntry.setStatus("completed"); + assertEquals("completed", mealPlanEntry.getStatus()); + } + + @Test + public void testStatusDefaultValue() { + // Ensure the default value of 'status' is "planned" when created + MealPlanEntry entry = new MealPlanEntry(2, recipe, LocalDate.of(2024, 11, 27), 103, "Breakfast"); + assertEquals("planned", entry.getStatus()); + } +} \ No newline at end of file diff --git a/src/test/java/entityTest/NutrientTest.java b/src/test/java/entityTest/NutrientTest.java new file mode 100644 index 000000000..c8e6f912d --- /dev/null +++ b/src/test/java/entityTest/NutrientTest.java @@ -0,0 +1,51 @@ +package entityTest; + +import entity.Nutrient; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class NutrientTest { + + private Nutrient nutrient; + + @Before + public void setUp() { + // Initialize the Nutrient object before each test + nutrient = new Nutrient("Vitamin A"); + } + + @Test + public void testConstructor() { + // Verify that the constructor correctly sets the nutrientInfo field + assertNotNull(nutrient); + assertEquals("Vitamin A", nutrient.getNutrientInfo()); + } + + @Test + public void testGetNutrientInfo() { + // Verify that the getter returns the correct nutrientInfo + assertEquals("Vitamin A", nutrient.getNutrientInfo()); + } + + @Test + public void testSetNutrientInfo() { + // Set a new nutrientInfo value and verify that it's updated + nutrient.setNutrientInfo("Vitamin C"); + assertEquals("Vitamin C", nutrient.getNutrientInfo()); + } + + @Test + public void testSetNutrientInfo_emptyString() { + // Set an empty string and verify that the setter works correctly + nutrient.setNutrientInfo(""); + assertEquals("", nutrient.getNutrientInfo()); + } + + @Test + public void testSetNutrientInfo_nullValue() { + // Set null and verify that the setter allows null values + nutrient.setNutrientInfo(null); + assertNull(nutrient.getNutrientInfo()); + } +} diff --git a/src/test/java/entityTest/NutritionTest.java b/src/test/java/entityTest/NutritionTest.java new file mode 100644 index 000000000..192a9df04 --- /dev/null +++ b/src/test/java/entityTest/NutritionTest.java @@ -0,0 +1,114 @@ +package entityTest; + +import entity.Nutrition; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class NutritionTest { + + private Nutrition nutrition; + + @Before + public void setUp() { + // Initialize the Nutrition object with sample data before each test + nutrition = new Nutrition(100.0, 5.0, 2.0, 30.0, 10.0, 15.0); + } + + @Test + public void testConstructor() { + // Verify that the object is correctly initialized by the constructor + assertNotNull(nutrition); + assertEquals(100.0, nutrition.getCalories(), 0.0); + assertEquals(5.0, nutrition.getProtein(), 0.0); + assertEquals(2.0, nutrition.getFat(), 0.0); + assertEquals(30.0, nutrition.getCarbohydrates(), 0.0); + assertEquals(10.0, nutrition.getFiber(), 0.0); + assertEquals(15.0, nutrition.getSugar(), 0.0); + } + + @Test + public void testFormatNutritionInfo() { + // Verify the formatNutritionInfo method produces the correct formatted string + String expectedInfo = "Calories: 100.00, Protein: 5.00 g, Fat: 2.00 g, Carbohydrates: 30.00 g, Fiber: 10.00 g, Sugar: 15.00 g"; + assertEquals(expectedInfo, nutrition.formatNutritionInfo()); + } + + @Test + public void testGetCalories() { + // Verify that the getter for calories returns the correct value + assertEquals(100.0, nutrition.getCalories(), 0.0); + } + + @Test + public void testSetCalories() { + // Set new value for calories and verify it's updated correctly + nutrition.setCalories(200.0); + assertEquals(200.0, nutrition.getCalories(), 0.0); + } + + @Test + public void testGetProtein() { + // Verify that the getter for protein returns the correct value + assertEquals(5.0, nutrition.getProtein(), 0.0); + } + + @Test + public void testSetProtein() { + // Set new value for protein and verify it's updated correctly + nutrition.setProtein(10.0); + assertEquals(10.0, nutrition.getProtein(), 0.0); + } + + @Test + public void testGetFat() { + // Verify that the getter for fat returns the correct value + assertEquals(2.0, nutrition.getFat(), 0.0); + } + + @Test + public void testSetFat() { + // Set new value for fat and verify it's updated correctly + nutrition.setFat(4.0); + assertEquals(4.0, nutrition.getFat(), 0.0); + } + + @Test + public void testGetCarbohydrates() { + // Verify that the getter for carbohydrates returns the correct value + assertEquals(30.0, nutrition.getCarbohydrates(), 0.0); + } + + @Test + public void testSetCarbohydrates() { + // Set new value for carbohydrates and verify it's updated correctly + nutrition.setCarbohydrates(40.0); + assertEquals(40.0, nutrition.getCarbohydrates(), 0.0); + } + + @Test + public void testGetFiber() { + // Verify that the getter for fiber returns the correct value + assertEquals(10.0, nutrition.getFiber(), 0.0); + } + + @Test + public void testSetFiber() { + // Set new value for fiber and verify it's updated correctly + nutrition.setFiber(15.0); + assertEquals(15.0, nutrition.getFiber(), 0.0); + } + + @Test + public void testGetSugar() { + // Verify that the getter for sugar returns the correct value + assertEquals(15.0, nutrition.getSugar(), 0.0); + } + + @Test + public void testSetSugar() { + // Set new value for sugar and verify it's updated correctly + nutrition.setSugar(20.0); + assertEquals(20.0, nutrition.getSugar(), 0.0); + } +} \ No newline at end of file diff --git a/src/test/java/entityTest/RecipeTest.java b/src/test/java/entityTest/RecipeTest.java new file mode 100644 index 000000000..a6600abd3 --- /dev/null +++ b/src/test/java/entityTest/RecipeTest.java @@ -0,0 +1,107 @@ +package entityTest; + +import entity.*; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +public class RecipeTest { + + private Recipe recipe; + private Ingredient ingredient1; + private Ingredient ingredient2; + private Nutrition nutrition; + private Food food1; + private Food food2; + + @Before + public void setUp() { + // Create real instances of dependencies for testing + ingredient1 = new Ingredient(1, "Sugar", 100, "g"); + ingredient2 = new Ingredient(2, "Flour", 200, "g"); + List ingredients = Arrays.asList(ingredient1, ingredient2); + + // Create real instance of Nutrition (assuming Nutrition is simple enough to use directly) + nutrition = new Nutrition(200, 3.0, 5.0, 30.0, 4.0, 10.0); + + // Create real instances of Food + food1 = new Food(1, "Apple", "Fruit", "g", 150, nutrition); + food2 = new Food(2, "Banana", "Fruit", "g", 200, nutrition); + List foods = Arrays.asList(food1, food2); + + // Create Recipe instance with real objects + recipe = new Recipe(1, "Cake", "Delicious cake", ingredients, "Mix ingredients and bake", nutrition, foods, null, 4); + } + + @Test + public void testConstructor() { + assertEquals(1, recipe.getRecipeId()); + assertEquals("Cake", recipe.getTitle()); + assertEquals("Delicious cake", recipe.getDescription()); + assertEquals(4, recipe.getServings()); + assertEquals(2, recipe.getIngredients().size()); // Ensure both ingredients are set + assertEquals("Mix ingredients and bake", recipe.getInstructions()); + assertNotNull(recipe.getNutrition()); // Ensure nutrition is correctly injected + assertEquals(2, recipe.getFood().size()); // Ensure foods are correctly injected + } + + @Test + public void testGettersAndSetters() { + recipe.setRecipeId(2); + recipe.setTitle("Chocolate Cake"); + recipe.setDescription("Rich chocolate cake"); + recipe.setServings(6); + recipe.setInstructions("Mix and bake at 350 degrees"); + recipe.setIngredients(Arrays.asList(new Ingredient(3, "Cocoa", 50, "g"))); + recipe.setFood(Arrays.asList(new Food(3, "Cocoa", "Baking", "g", 50, nutrition))); + recipe.setNutrition(new Nutrition(300, 4.0, 10.0, 40.0, 5.0, 15.0)); + + assertEquals(2, recipe.getRecipeId()); + assertEquals("Chocolate Cake", recipe.getTitle()); + assertEquals("Rich chocolate cake", recipe.getDescription()); + assertEquals(6, recipe.getServings()); + assertEquals("Mix and bake at 350 degrees", recipe.getInstructions()); + assertEquals(1, recipe.getIngredients().size()); // Ensure new ingredient is set + assertEquals(1, recipe.getFood().size()); // Ensure new food is set + assertNotNull(recipe.getNutrition()); // Ensure nutrition is set + } + + @Test + public void testToString() { + String expectedString = "Recipe: Cake (Servings: 4)\nDescription: Delicious cake\nIngredients: [Ingredient{id=1, name='Sugar', quantity=100.0, unit='g'}, Ingredient{id=2, name='Flour', quantity=200.0, unit='g'}]\nInstructions: Mix ingredients and bake"; + assertEquals(expectedString, recipe.toString()); + } + + @Test + public void testNullDescription() { + // Test if the description defaults correctly when null is passed + Recipe recipeWithNullDescription = new Recipe(2, "Pancakes", null, Arrays.asList(ingredient1, ingredient2), "Mix and fry", nutrition, Arrays.asList(food1, food2), null, 2); + assertEquals("Instructions not available", recipeWithNullDescription.getDescription()); + } + + @Test + public void testEmptyDescription() { + // Test if the description defaults correctly when an empty string is passed + Recipe recipeWithEmptyDescription = new Recipe(2, "Pancakes", "", Arrays.asList(ingredient1, ingredient2), "Mix and fry", nutrition, Arrays.asList(food1, food2), null, 2); + assertEquals("Instructions not available", recipeWithEmptyDescription.getDescription()); + } + + @Test + public void testSetValidInstructions() { + // Setting valid instructions + recipe.setInstructions("Bake at 180 degrees"); + assertEquals("Bake at 180 degrees", recipe.getInstructions()); + } + + @Test + public void testSetInvalidInstructions() { + // Test that setting null instructions is allowed (based on current implementation) + recipe.setInstructions(null); + assertNull(recipe.getInstructions()); + } + +} \ No newline at end of file diff --git a/src/test/java/use_case/note/NoteInteractorTest.java b/src/test/java/use_case/note/NoteInteractorTest.java deleted file mode 100644 index a3ed466b6..000000000 --- a/src/test/java/use_case/note/NoteInteractorTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package use_case.note; - -import entity.User; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class NoteInteractorTest { - - @Test - public void testExecuteRefreshSuccess() { - - NoteDataAccessInterface noteDAO = new NoteDataAccessInterface() { - - - @Override - public String saveNote(User user, String note) { - return ""; - } - - - @Override - public String loadNote(User user) { - return "test"; - } - }; - - NoteOutputBoundary noteOB = new NoteOutputBoundary() { - @Override - public void prepareSuccessView(String message) { - assertEquals("test", message); - } - - @Override - public void prepareFailView(String errorMessage) { - fail(errorMessage); - } - }; - - NoteInteractor noteInteractor = new NoteInteractor(noteDAO, noteOB); - - noteInteractor.executeRefresh(); - - - } -} \ No newline at end of file diff --git a/src/test/java/use_caseTest/MealPlanningInteractorTest.java b/src/test/java/use_caseTest/MealPlanningInteractorTest.java new file mode 100644 index 000000000..c4c8c8f0a --- /dev/null +++ b/src/test/java/use_caseTest/MealPlanningInteractorTest.java @@ -0,0 +1,164 @@ +package use_caseTest; + +import data_access.*; +import data_access.SavedRecipesDataAccessObject; +import entity.MealPlanEntry; +import entity.Recipe; +import use_case.meal_planning.MealPlanningDataAccessInterface; +import use_case.meal_planning.MealPlanningInteractor; +import use_case.meal_planning.MealPlanningOutputBoundary; +import data_access.SavedRecipesDataAccessInterface; +import org.junit.Before; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class MealPlanningInteractorTest { + + private MealPlanningDataAccessInterface dataAccess; + private SavedRecipesDataAccessInterface savedRecipesDataAccessInterface; + private MealPlanningOutputBoundary outputBoundary; + private MealPlanningInteractor interactor; + + @Before + public void setUp() { + // Simple in-memory implementations of dependencies + dataAccess = new MealPlanningDataAccessObject(new SavedRecipesDataAccessObject()); + savedRecipesDataAccessInterface = new SavedRecipesDataAccessObject(); + outputBoundary = new MealPlanningOutputBoundary() { + @Override + public void presentSavedRecipes(List recipes) { + // Do nothing (or log, if needed) + } + + @Override + public void presentError(String message) { + System.out.println("Error: " + message); + } + + @Override + public void presentStatusUpdateSuccess(String message) { + System.out.println("Status Update: " + message); + } + + @Override + public void presentAddSuccess(String message) { + System.out.println("Add Success: " + message); + } + + @Override + public void presentRemoveSuccess(String message) { + System.out.println("Remove Success: " + message); + } + + @Override + public void presentCalendarWeek(List mealPlanEntries) { + // Do nothing (or log, if needed) + } + }; + + // Create the interactor + interactor = new MealPlanningInteractor(dataAccess, savedRecipesDataAccessInterface, outputBoundary); + } + + @Test + public void testAddToCalendar() { + // Create a dummy recipe + Recipe recipe = new Recipe(1, "Spaghetti Bolognese", "Delicious pasta with meat sauce", new ArrayList<>(), "Boil pasta, cook meat sauce", null, new ArrayList<>(), null, 4); + savedRecipesDataAccessInterface.saveRecipe(1, recipe); + + // Add recipe to calendar for user 1 + LocalDate date = LocalDate.now(); + interactor.addToCalendar(1, 1, date, "Dinner"); + + // Verify that the recipe was added (check for its existence in the meal plan entries) + List weeklyPlan = dataAccess.getWeeklyPlan(1, date.withDayOfMonth(1)); // Using the start of the current month as an example + assertTrue(weeklyPlan.size() > 0); + assertEquals("Spaghetti Bolognese", weeklyPlan.get(0).getRecipe().getTitle()); + } + + @Test + public void testUpdateMealStatus() { + // Create a dummy recipe + Recipe recipe = new Recipe(1, "Chicken Salad", "Healthy salad with chicken", new ArrayList<>(), "Mix ingredients", null, new ArrayList<>(), null, 2); + savedRecipesDataAccessInterface.saveRecipe(1, recipe); + + // Add recipe to calendar + LocalDate date = LocalDate.now(); + interactor.addToCalendar(1, 1, date, "Lunch"); + + // Update meal status + List weeklyPlan = dataAccess.getWeeklyPlan(1, date.withDayOfMonth(1)); + int entryId = weeklyPlan.get(0).getEntryId(); + interactor.updateMealStatus(1, entryId, "Completed"); + + // Verify the status is updated + MealPlanEntry updatedEntry = dataAccess.getMealPlanEntry(1, entryId); + assertNotNull(updatedEntry); + assertEquals("Completed", updatedEntry.getStatus()); + } + + @Test + public void testRemoveFromCalendar() { + // Create a dummy recipe + Recipe recipe = new Recipe(1, "Grilled Cheese", "Classic grilled cheese sandwich", new ArrayList<>(), "Grill bread and cheese", null, new ArrayList<>(), null, 1); + savedRecipesDataAccessInterface.saveRecipe(1, recipe); + + // Add recipe to calendar + LocalDate date = LocalDate.now(); + interactor.addToCalendar(1, 1, date, "Snack"); + + // Remove the recipe + List weeklyPlan = dataAccess.getWeeklyPlan(1, date.withDayOfMonth(1)); + int entryId = weeklyPlan.get(0).getEntryId(); + interactor.removeFromCalendar(1, entryId); + + // Verify the recipe is removed + MealPlanEntry removedEntry = dataAccess.getMealPlanEntry(1, entryId); + assertNull(removedEntry); + } + + @Test + public void testGetCalendarWeek() { + // Create and add multiple recipes + Recipe recipe1 = new Recipe(1, "Pancakes", "Fluffy pancakes", new ArrayList<>(), "Cook pancakes", null, new ArrayList<>(), null, 2); + savedRecipesDataAccessInterface.saveRecipe(1, recipe1); + + Recipe recipe2 = new Recipe(2, "Omelette", "Cheese omelette", new ArrayList<>(), "Whisk eggs and cook", null, new ArrayList<>(), null, 1); + savedRecipesDataAccessInterface.saveRecipe(1, recipe2); + + LocalDate weekStart = LocalDate.now().with(java.time.DayOfWeek.MONDAY); + interactor.addToCalendar(1, 1, weekStart, "Breakfast"); + interactor.addToCalendar(1, 2, weekStart.plusDays(1), "Breakfast"); + + // Get the weekly plan + List weeklyPlan = dataAccess.getWeeklyPlan(1, weekStart); + + // Verify that the correct recipes are in the weekly plan + assertEquals(2, weeklyPlan.size()); + assertTrue(weeklyPlan.stream().anyMatch(entry -> entry.getRecipe().getTitle().equals("Pancakes"))); + assertTrue(weeklyPlan.stream().anyMatch(entry -> entry.getRecipe().getTitle().equals("Omelette"))); + } + + @Test + public void testInitializeMealPlanning() { + // Test the initialization of the meal planning for a user + Recipe recipe = new Recipe(1, "Salad", "Fresh salad", new ArrayList<>(), "Mix ingredients", null, new ArrayList<>(), null, 1); + savedRecipesDataAccessInterface.saveRecipe(1, recipe); + + LocalDate weekStart = LocalDate.now().with(java.time.DayOfWeek.MONDAY); + interactor.addToCalendar(1, 1, weekStart, "Lunch"); + + // Initialize meal planning + interactor.initializeMealPlanning(1); + + // Verify that the meal plan for the current week is loaded + List weeklyPlan = dataAccess.getWeeklyPlan(1, weekStart); + assertFalse(weeklyPlan.isEmpty()); + assertEquals("Salad", weeklyPlan.get(0).getRecipe().getTitle()); + } +} \ No newline at end of file diff --git a/src/test/java/use_caseTest/NutritionAnalysisInteractorTest.java b/src/test/java/use_caseTest/NutritionAnalysisInteractorTest.java new file mode 100644 index 000000000..4255da9ac --- /dev/null +++ b/src/test/java/use_caseTest/NutritionAnalysisInteractorTest.java @@ -0,0 +1,88 @@ +package use_caseTest; + +import data_access.*; +import entity.*; +import interface_adapter.nutrition_analysis.NutritionAnalysisPresenter; +import interface_adapter.nutrition_analysis.NutritionAnalysisState; +import interface_adapter.nutrition_analysis.NutritionAnalysisViewModel; +import interface_adapter.recipe_search.RecipeSearchPresenter; +import interface_adapter.recipe_search.RecipeSearchViewModel; +import org.json.JSONArray; +import org.junit.Before; +import org.junit.Test; +import use_case.nutrition_analysis.NutritionAnalysisException; +import use_case.nutrition_analysis.NutritionAnalysisInteractor; +import use_case.recipe_search.RecipeSearchInteractor; + +import static org.junit.Assert.*; + +import java.util.List; + +public class NutritionAnalysisInteractorTest { + + private NutritionAnalysisInteractor nutritionAnalysis; + private NutritionAnalysisViewModel viewModel; + private Recipe recipe; + + @Before + public void setUp() { + + // Initialize necessary components + NutritionAnalysisDataAccessObject mockDAO = new NutritionAnalysisDataAccessObject(); // Custom implementation + viewModel = new NutritionAnalysisViewModel(); + NutritionAnalysisPresenter presenter = new NutritionAnalysisPresenter(viewModel); + nutritionAnalysis = new NutritionAnalysisInteractor(mockDAO, presenter); + + // Establishment of a system that produce a valid Recipe Object + SavedRecipesDataAccessObject savedRecipesDataAccess = new SavedRecipesDataAccessObject(); + RecipeSearchViewModel recipeSearchViewModel = new RecipeSearchViewModel(); + RecipeSearchPresenter recipeSearchPresenter = new RecipeSearchPresenter(recipeSearchViewModel); + RecipeSearchDataAccessObject recipeSearchDataAccessObject = new RecipeSearchDataAccessObject(); + + RecipeSearchInteractor recipeSearchInputBoundaryUseCase = new RecipeSearchInteractor( + recipeSearchDataAccessObject, + savedRecipesDataAccess, + recipeSearchPresenter + ); + + // Assume RecipeSearch use case (RecipeSearchInteractor) is well tested (independence principle). + final String exampleFoodName = "egg"; + List exampleIngredients = List.of(exampleFoodName); + recipeSearchInputBoundaryUseCase.searchRecipes(exampleIngredients); + recipe = recipeSearchViewModel.getState().getRecipes().get(1); + + } + + @Test + public void testAnalyzeNutrition_Success() { + + + // Call the method + nutritionAnalysis.analyzeNutrition(recipe); + + // Get the state from the viewModel + NutritionAnalysisState state = viewModel.getState(); + + // Ensure that the state is of type NutritionAnalysisState + assertNotNull("Expected state to be an instance of NutritionAnalysisState", state); + + // Assert the result + assertNotNull(state); + assertEquals(33, state.getNutritionResults().size()); + assertTrue(state.getNutritionResults().toString().contains("Vitamin B-6")); + assertTrue(state.getNutritionResults().toString().contains("Fatty acids, total monounsaturated")); + } + + @Test + public void testAnalyzeNutrition_ErrorHandling() { + + try { + recipe.setJsonIngredient(new JSONArray()); + nutritionAnalysis.analyzeNutrition(recipe); + } + catch (NutritionAnalysisException e) { + assertEquals("Error processing nutrition analysis", e.getMessage()); + assertTrue(e.getCause() instanceof RuntimeException); + } + } +} diff --git a/src/test/java/use_caseTest/RecipeSearchInteractorTest.java b/src/test/java/use_caseTest/RecipeSearchInteractorTest.java new file mode 100644 index 000000000..fb67f8f7a --- /dev/null +++ b/src/test/java/use_caseTest/RecipeSearchInteractorTest.java @@ -0,0 +1,155 @@ +package use_caseTest; + +import data_access.RecipeSearchDataAccessObject; +import data_access.SavedRecipesDataAccessObject; +import entity.Nutrition; +import entity.Recipe; +import org.junit.jupiter.api.Test; +import use_case.recipe_search.RecipeSearchInteractor; +import use_case.recipe_search.RecipeSearchOutputBoundary; +import use_case.recipe_search.RecipeSearchException; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class RecipeSearchInteractorTest { + + @Test + void testSaveRecipeSuccess() throws RecipeSearchException { + // Arrange + InMemorySavedRecipesDataAccess savedRecipesDataAccess = new InMemorySavedRecipesDataAccess(); + TestRecipeSearchOutputBoundary outputBoundary = new TestRecipeSearchOutputBoundary(); + RecipeSearchInteractor interactor = new RecipeSearchInteractor(null, savedRecipesDataAccess, outputBoundary); + + Recipe recipe = createTestRecipe(); + + // Act + interactor.saveRecipe(1, recipe); + + // Assert + assertTrue(savedRecipesDataAccess.isRecipeSaved(1, recipe)); + assertEquals(recipe, outputBoundary.savedRecipe); + } + + @Test + void testSaveRecipeFailure() { + // Arrange + FaultySavedRecipesDataAccess savedRecipesDataAccess = new FaultySavedRecipesDataAccess(); + TestRecipeSearchOutputBoundary outputBoundary = new TestRecipeSearchOutputBoundary(); + RecipeSearchInteractor interactor = new RecipeSearchInteractor(null, savedRecipesDataAccess, outputBoundary); + + Recipe recipe = createTestRecipe(); + + // Act & Assert + RecipeSearchException exception = assertThrows(RecipeSearchException.class, () -> interactor.saveRecipe(1, recipe)); + assertEquals("Failed to save recipe", exception.getMessage()); + assertEquals("Failed to save recipe: Save operation failed", outputBoundary.errorMessage); + } + + @Test + void testSearchRecipesSuccess() throws RecipeSearchException { + // Arrange + InMemoryRecipeSearchDataAccessObject searchDataAccessObject = new InMemoryRecipeSearchDataAccessObject(); + TestRecipeSearchOutputBoundary outputBoundary = new TestRecipeSearchOutputBoundary(); + RecipeSearchInteractor interactor = new RecipeSearchInteractor(searchDataAccessObject, null, outputBoundary); + + List ingredients = List.of("ingredient1", "ingredient2"); + + // Act + interactor.searchRecipes(ingredients); + + // Assert + assertEquals(1, outputBoundary.presentedRecipes.size()); + assertEquals("Test Recipe", outputBoundary.presentedRecipes.get(0).getTitle()); + } + + @Test + void testSearchRecipesFailure() { + // Arrange + FaultyRecipeSearchDataAccessObject searchDataAccessObject = new FaultyRecipeSearchDataAccessObject(); + TestRecipeSearchOutputBoundary outputBoundary = new TestRecipeSearchOutputBoundary(); + RecipeSearchInteractor interactor = new RecipeSearchInteractor(searchDataAccessObject, null, outputBoundary); + + // Act & Assert + RecipeSearchException exception = assertThrows(RecipeSearchException.class, () -> interactor.searchRecipes(List.of("ingredient1"))); + assertEquals("Recipe search failed", exception.getMessage()); + assertEquals("Failed to search recipes: Search operation failed", outputBoundary.errorMessage); + } + + // Helper to create a test recipe + private Recipe createTestRecipe() { + return new Recipe( + 1, + "Test Recipe", + "Description", + new ArrayList<>(), + "Instructions", + new Nutrition(100, 10, 10, 10, 10, 10), + new ArrayList<>(), + null, + 4 + ); + } + + // Stub for SavedRecipesDataAccessInterface + private static class InMemorySavedRecipesDataAccess extends SavedRecipesDataAccessObject { + private final List savedRecipes = new ArrayList<>(); + + @Override + public void saveRecipe(int userId, Recipe recipe) { + savedRecipes.add(recipe); + } + + public boolean isRecipeSaved(int userId, Recipe recipe) { + return savedRecipes.contains(recipe); + } + } + + private static class FaultySavedRecipesDataAccess extends SavedRecipesDataAccessObject { + @Override + public void saveRecipe(int userId, Recipe recipe) { + throw new RuntimeException("Save operation failed"); + } + } + + // Stub for RecipeSearchDataAccessObject + private static class InMemoryRecipeSearchDataAccessObject extends RecipeSearchDataAccessObject { + @Override + public List searchRecipesByFoodName(String searchQuery) { + List recipes = new ArrayList<>(); + recipes.add(new Recipe(1, "Test Recipe", "Test description", new ArrayList<>(), "Test instructions", new Nutrition(0, 0, 0, 0, 0, 0), new ArrayList<>(), null, 2)); + return recipes; + } + } + + private static class FaultyRecipeSearchDataAccessObject extends RecipeSearchDataAccessObject { + @Override + public List searchRecipesByFoodName(String searchQuery) { + throw new RuntimeException("Search operation failed"); + } + } + + // Test Output Boundary + private static class TestRecipeSearchOutputBoundary implements RecipeSearchOutputBoundary { + Recipe savedRecipe; + String errorMessage; + List presentedRecipes; + + @Override + public void presentSaveSuccess(Recipe recipe) { + this.savedRecipe = recipe; + } + + @Override + public void presentError(String message) { + this.errorMessage = message; + } + + @Override + public void presentRecipes(List recipes) { + this.presentedRecipes = recipes; + } + } +} \ No newline at end of file diff --git a/src/test/java/use_caseTest/RecipeSearchWithRestrictionInteractorTest.java b/src/test/java/use_caseTest/RecipeSearchWithRestrictionInteractorTest.java new file mode 100644 index 000000000..8abdb3d08 --- /dev/null +++ b/src/test/java/use_caseTest/RecipeSearchWithRestrictionInteractorTest.java @@ -0,0 +1,100 @@ +package use_caseTest; + +import entity.Nutrition; +import entity.Recipe; +import org.junit.jupiter.api.Test; +import use_case.search_with_restriction.RecipeSearchWithRestrictionInteractor; +import data_access.SearchWithRestrictionDataAccessObject; +import use_case.search_with_restriction.SearchWithRestrictionException; +import use_case.search_with_restriction.SearchWithRestrictionOutputBoundary; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class RecipeSearchWithRestrictionInteractorTest { + + @Test + void testSearchWithRestrictionsSuccess() throws SearchWithRestrictionException { + // Arrange + InMemorySearchWithRestrictionDataAccess dataAccessObject = new InMemorySearchWithRestrictionDataAccess(); + TestOutputBoundary outputBoundary = new TestOutputBoundary(); + RecipeSearchWithRestrictionInteractor interactor = new RecipeSearchWithRestrictionInteractor(dataAccessObject, outputBoundary); + + Map> restrictions = new HashMap<>(); + restrictions.put("Food Name", List.of("apple", "banana")); + restrictions.put("Diet Label", List.of("vegan")); + restrictions.put("Health Label", List.of("low-sugar")); + restrictions.put("Cuisine Type", List.of("Asian")); + + // Act + interactor.searchRestrictionRecipes(restrictions); + + // Assert + assertEquals(1, outputBoundary.presentedRecipes.size()); + assertEquals("Test Recipe", outputBoundary.presentedRecipes.get(0).getTitle()); + } + + @Test + void testSearchWithRestrictionsFailure() { + // Arrange + FaultySearchWithRestrictionDataAccess dataAccessObject = new FaultySearchWithRestrictionDataAccess(); + TestOutputBoundary outputBoundary = new TestOutputBoundary(); + RecipeSearchWithRestrictionInteractor interactor = new RecipeSearchWithRestrictionInteractor(dataAccessObject, outputBoundary); + + Map> restrictions = new HashMap<>(); + restrictions.put("Food Name", List.of("apple")); + + // Act & Assert + SearchWithRestrictionException exception = assertThrows(SearchWithRestrictionException.class, + () -> interactor.searchRestrictionRecipes(restrictions)); + assertEquals("Recipe search failed", exception.getMessage()); + assertEquals("Failed to search recipes: Search operation failed", outputBoundary.errorMessage); + } + + // Stub for SearchWithRestrictionDataAccessObject + private static class InMemorySearchWithRestrictionDataAccess extends SearchWithRestrictionDataAccessObject { + @Override + public List searchRecipesByRestriction(String foodQuery, String dietQuery, String healthQuery, String cuisineQuery) { + List recipes = new ArrayList<>(); + recipes.add(new Recipe( + 1, + "Test Recipe", + "Test description", + new ArrayList<>(), + "Test instructions", + new Nutrition(0, 0, 0, 0, 0, 0), + new ArrayList<>(), + null, + 2 + )); + return recipes; + } + } + + private static class FaultySearchWithRestrictionDataAccess extends SearchWithRestrictionDataAccessObject { + @Override + public List searchRecipesByRestriction(String foodQuery, String dietQuery, String healthQuery, String cuisineQuery) { + throw new RuntimeException("Search operation failed"); + } + } + + // Test Output Boundary + private static class TestOutputBoundary implements SearchWithRestrictionOutputBoundary { + List presentedRecipes; + String errorMessage; + + @Override + public void presentRecipes(List recipes) { + this.presentedRecipes = recipes; + } + + @Override + public void presentError(String message) { + this.errorMessage = message; + } + } +} diff --git a/src/test/java/use_caseTest/serving_adjust/ServingAdjustInteractorTest.java b/src/test/java/use_caseTest/serving_adjust/ServingAdjustInteractorTest.java new file mode 100644 index 000000000..5dcc060e1 --- /dev/null +++ b/src/test/java/use_caseTest/serving_adjust/ServingAdjustInteractorTest.java @@ -0,0 +1,209 @@ +package use_caseTest.serving_adjust; + +import entity.Ingredient; +import entity.Recipe; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import use_case.serving_adjust.ServingAdjustInteractor; +import use_case.serving_adjust.ServingAdjustOutputBoundary; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for ServingAdjustInteractor without using Mockito. + */ +class ServingAdjustInteractorTest { + + private TestServingAdjustOutputBoundary outputBoundary; + private ServingAdjustInteractor interactor; + + @BeforeEach + public void setUp() { + + outputBoundary = new TestServingAdjustOutputBoundary(); + + interactor = new ServingAdjustInteractor(outputBoundary); + } + + @Test + void testAdjustServings_singleRecipe() { + + Ingredient ingredient1 = new Ingredient(1, "Flour", 200, "grams"); + Ingredient ingredient2 = new Ingredient(2, "Sugar", 100, "grams"); + List ingredients = Arrays.asList(ingredient1, ingredient2); + + Recipe recipe = new Recipe( + 1, + "Cake", + "Delicious cake", + ingredients, + "Mix ingredients and bake.", + null, + null, + null, + 4 + ); + + interactor.adjustServings(8, recipe); + + assertEquals(8, recipe.getServings()); + assertEquals(400, ingredient1.getQuantity()); + assertEquals(200, ingredient2.getQuantity()); + } + + @Test + void testAdjustServings_invalidServings() { + + Ingredient ingredient = new Ingredient(1, "Salt", 10, "grams"); + List ingredients = Collections.singletonList(ingredient); + + Recipe recipe = new Recipe( + 2, + "Soup", + "Tasty soup", + ingredients, + "Boil water and add salt.", + null, + null, + null, + 2 + ); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> interactor.adjustServings(0, recipe) + ); + assertEquals("Servings must be greater than zero.", exception.getMessage()); + } + + @Test + void testAdjustServings_multipleRecipes() { + + Ingredient ingredient1 = new Ingredient(1, "Pasta", 100, "grams"); + Recipe recipe1 = new Recipe( + 3, + "Spaghetti", + "Classic spaghetti", + Collections.singletonList(ingredient1), + "Cook pasta.", + null, + null, + null, + 2 + ); + + Ingredient ingredient2 = new Ingredient(2, "Rice", 200, "grams"); + Recipe recipe2 = new Recipe( + 4, + "Fried Rice", + "Delicious fried rice", + Collections.singletonList(ingredient2), + "Fry rice.", + null, + null, + null, + 4 + ); + + List recipes = Arrays.asList(recipe1, recipe2); + + interactor.adjustServingsForMultiple(8, recipes); + + assertEquals(8, recipe1.getServings()); + assertEquals(400, ingredient1.getQuantity()); + + assertEquals(8, recipe2.getServings()); + assertEquals(400, ingredient2.getQuantity()); + + assertTrue(outputBoundary.wasCalled); + assertEquals(recipes, outputBoundary.updatedRecipes); + } + + @Test + void testAdjustServings_listMethod() { + + Ingredient ingredient1 = new Ingredient(1, "Tomato", 2, "pieces"); + Ingredient ingredient2 = new Ingredient(2, "Lettuce", 1, "head"); + Recipe recipe1 = new Recipe( + 5, + "Salad", + "Fresh salad", + Arrays.asList(ingredient1, ingredient2), + "Chop and mix.", + null, + null, + null, + 2 + ); + + Ingredient ingredient3 = new Ingredient(3, "Bread", 4, "slices"); + Recipe recipe2 = new Recipe( + 6, + "Sandwich", + "Ham sandwich", + Collections.singletonList(ingredient3), + "Assemble ingredients.", + null, + null, + null, + 2 + ); + + List recipes = Arrays.asList(recipe1, recipe2); + + interactor.adjustServings(4, recipes); + + assertEquals(4, recipe1.getServings()); + assertEquals(4, ingredient1.getQuantity()); // 2 * (4/2) = 4 + assertEquals(2, ingredient2.getQuantity()); // 1 * (4/2) = 2 + + assertEquals(4, recipe2.getServings()); + assertEquals(8, ingredient3.getQuantity()); // 4 * (4/2) = 8 + + assertTrue(outputBoundary.wasCalled); + assertEquals(recipes, outputBoundary.updatedRecipes); + } + + @Test + void testAdjustServings_listMethod_invalidServings() { + + Ingredient ingredient = new Ingredient(1, "Milk", 500, "ml"); + Recipe recipe = new Recipe( + 7, + "Smoothie", + "Fruit smoothie", + Collections.singletonList(ingredient), + "Blend ingredients.", + null, + null, + null, + 2 + ); + + List recipes = Collections.singletonList(recipe); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> interactor.adjustServings(0, recipes) + ); + assertEquals("Servings must be greater than zero.", exception.getMessage()); + } + + /** + * Test implementation of ServingAdjustOutputBoundary for capturing method calls. + */ + private static class TestServingAdjustOutputBoundary implements ServingAdjustOutputBoundary { + public boolean wasCalled = false; + public List updatedRecipes = null; + + @Override + public void presentUpdatedRecipes(List recipes) { + this.wasCalled = true; + this.updatedRecipes = recipes; + } + } +} \ No newline at end of file