diff --git a/.github/ISSUE_TEMPLATE/administrative-matters.md b/.github/ISSUE_TEMPLATE/administrative-matters.md new file mode 100644 index 0000000000..7585425611 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/administrative-matters.md @@ -0,0 +1,36 @@ +--- +name: Administrative Matters +about: Add chores that must be done +title: "[Task] Your Chore Name" +labels: priority.Low, type.Task +assignees: '' + +--- + +**Describe the Task** +A clear and concise description of what the Task/Chore is. +e.g. Move testing code to new folder + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. + +**Labels** +Remember to tag the priority using the labels function. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..17cbf2668c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: File a bug report for WellNUS++ +title: "[Bug] Your Bug Name" +labels: severity.Low, type.Bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. + +**Severity** +Remember to tag the severity of the bug and priority (if applicable) using the labels function. diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 0000000000..48f1383235 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,21 @@ +--- +name: Enhancement +about: Anything that improves on an existing feature/implementation +title: "[Enhancement] Your Enhancement" +labels: priority.Low, type.Enhancement +assignees: '' + +--- + +**What is the current behaviour? Please describe any relevant features.** + +**Why should this behaviour be changed/improved? Describe any relevant user stories/frustrations.** + +**Describe your proposed enhancement.** + +**Screenshots(if any) of the enhancement.** + +**Any alternative implementations/enhancements that were considered?** +If any exist, explain why they were not chosen. + +**Why should this enhancement be added? Explain its benefits/significance.** diff --git a/.github/ISSUE_TEMPLATE/user-story-features.md b/.github/ISSUE_TEMPLATE/user-story-features.md new file mode 100644 index 0000000000..90d877c4e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/user-story-features.md @@ -0,0 +1,29 @@ +--- +name: User Story Features +about: Anything related to adding features that involve user stories +title: "[User Story Feature] Your Feature" +labels: priority.Low, type.Story +assignees: '' + +--- + +**User Story** +What is the user story this feature aims to achieve? +Describe it and label the issue accordingly. +- Is it a 'Epic' -> A big feature which can be broken down into smaller stories +- Is it a 'Story' -> A smaller feature + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. Optional if it does not value-add to the issue. + +**Additional context** +Add any other context or screenshots about the feature request here. + +**Labels** +Remember to label the feature with the appropriate `priority` and `type`. diff --git a/.gitignore b/.gitignore index 2873e189e1..c026971852 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT + +/data/ + +/log/ diff --git a/build.gradle b/build.gradle index d5e548e85f..5b0a3ce03f 100644 --- a/build.gradle +++ b/build.gradle @@ -29,18 +29,30 @@ test { } application { - mainClass = "seedu.duke.Duke" + mainClass = "wellnus.WellNus" } shadowJar { - archiveBaseName = "duke" - archiveClassifier = null + archiveBaseName.set('WellNus') + archiveClassifier.set('') + archiveVersion.set('v2.1') +} + +jar { + manifest { + attributes "Main-Class": "wellnus.WellNus" + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } } checkstyle { toolVersion = '10.2' } -run{ +run { standardInput = System.in + enableAssertions = true } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index c35db7c7e6..e8ee76467b 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -5,7 +5,7 @@ @@ -48,6 +48,21 @@ IMPORT CHECKS --> + + + + + + + + + + + + + + @@ -167,13 +188,10 @@ @@ -200,6 +218,13 @@ + + + + + + + @@ -237,13 +262,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -271,7 +403,7 @@ - + @@ -283,5 +415,15 @@ + + + + + + + + + + - + \ No newline at end of file diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 39efb6e4ac..39102a4119 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -1,10 +1,10 @@ + "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN" + "https://checkstyle.org/dtds/suppressions_1_2.dtd"> - + \ No newline at end of file diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..9dd39f60e6 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +| Display | Name | Github Profile | Portfolio | +|-----------------------------------------------|:--------------------------:|:-------------------------------------: |:-------------------------------------------------------------------------------:| +| ![Chen Wenxin](./team/wenxin.jpg) | Chen Wenxin | [Github](https://github.com/wenxin-c) | [Portfolio](https://ay2223s2-cs2113-t12-4.github.io/tp/team/wenxin-c.html) | +| ![Wang Haoyang](./team/haoyangw.png) | Wang Haoyang | [Github](https://github.com/haoyangw) | [Portfolio](https://ay2223s2-cs2113-t12-4.github.io/tp/team/haoyangw.html) | +| ![Wang Yongbin](./team/yongbin.png) | Wang Yongbin | [Github](https://github.com/YongbinWang) | [Portfolio](https://ay2223s2-cs2113-t12-4.github.io/tp/team/yongbinwang.html) | +| ![Bernard Lesley](./team/bernard.jpg) | Bernard Lesley Efendy | [Github](https://github.com/BernardLesley) | [Portfolio](https://ay2223s2-cs2113-t12-4.github.io/tp/team/bernardlesley.html) | +| ![Yek Jin Teck, Nicholas](./team/nichyjt.jpg) | Yek Jin Teck, Nicholas | [Github](https://github.com/nichyjt) | [Portfolio](https://ay2223s2-cs2113-t12-4.github.io/tp/team/nichyjt.html) | \ No newline at end of file diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS new file mode 100644 index 0000000000..a695ebb832 --- /dev/null +++ b/docs/CODEOWNERS @@ -0,0 +1,3 @@ +# By default, these are the owners of all code in the repo +# This means their approval is required for PRs before PRs can be merged +* @wenxin-c @nichyjt @haoyangw \ No newline at end of file diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..430b52ff0b 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,1515 @@ # Developer Guide -## Acknowledgements +# Table of Contents -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} + +* [Developer Guide](#developer-guide) +* [Table of Contents](#table-of-contents) +* [Acknowledgements](#acknowledgements) +* [Setting up, getting started](#setting-up-getting-started) + * [Setting up the project in your computer](#setting-up-the-project-in-your-computer) + * [Before writing code](#before-writing-code) +* [Design & implementation](#design--implementation) + * [Application Lifecycle](#application-lifecycle) + * [Overview](#overview) + * [Rationale](#rationale) + * [UI Component](#ui-component) + * [UI Implementation](#ui-implementation) + * [Self Reflection Component](#self-reflection-component) + * [Design considerations](#design-considerations) + * [User design considerations](#user-design-considerations) + * [Developer design considerations](#developer-design-considerations) + * [Self Reflection Implementation](#self-reflection-implementation) + * [Self Reflection commands implementation](#self-reflection-commands-implementation) + * [CommandParser Component](#commandparser-component) + * [Design Considerations](#design-considerations-1) + * [User design Considerations](#user-design-considerations-1) + * [Developer Design Considerations](#developer-design-considerations-1) + * [Alternative Designs Considered](#alternative-designs-considered) + * [CommandParser Syntax](#commandparser-syntax) + * [Implementation](#implementation) + * [Integration with WellNUS++](#integration-with-wellnus) + * [CommandParser API](#commandparser-api) + * [AtomicHabit Component](#atomichabit-component) + * [Design Considerations](#design-considerations-2) + * [User design considerations](#user-design-considerations-2) + * [Developer design considerations](#developer-design-considerations-2) + * [AtomicHabit Implementation](#atomichabit-implementation) + * [AtomicHabit Commands](#atomichabit-commands) + * [Gamification Component](#gamification-component) + * [Design Considerations](#design-considerations-3) + * [GamificationData](#gamificationdata) + * [GamificationStorage](#gamificationstorage) + * [GamificationUi](#gamificationui) + * [Commands](#commands) + * [Alternative Designs Considered](#alternative-designs-considered-1) + * [Defining gamification statistics logic within `GamificationManager`](#defining-gamification-statistics-logic-within-gamificationmanager) + * [Integrating `GamificationStorage` logic within `GamificationData`](#integrating-gamificationstorage-logic-within-gamificationdata) + * [Tokenizer](#tokenizer) + * [Design Considerations](#design-considerations-4) + * [Individual Tokenizers](#individual-tokenizers) + * [Storage](#storage) + * [Usage - `saveData`](#usage---savedata) + * [Usage - `loadData`](#usage---loaddata) + * [Design Considerations](#design-considerations-5) + * [Focus Timer Component](#focus-timer-component) + * [Focus Timer Implementation](#focus-timer-implementation) + * [State Management](#state-management) + * [Commands](#commands-1) +* [Appendix - Requirements](#appendix---requirements) + * [Product scope](#product-scope) + * [Product Name](#product-name) + * [Target user profile](#target-user-profile) + * [Value proposition](#value-proposition) + * [User Stories](#user-stories) + * [Non-Functional Requirements](#non-functional-requirements) + * [Glossary](#glossary) +* [Appendix - Instructions for manual testing](#appendix---instructions-for-manual-testing) + * [Launch](#launch) + * [Sample test cases](#sample-test-cases) + * [Help command](#help-command) + * [Atomic habits feature](#atomic-habits-feature) + * [Add atomic habits](#add-atomic-habits) + * [Reflection feature](#reflection-feature) + * [Get reflection questions](#get-reflection-questions) + * [Favorite reflection questions](#favorite-reflection-questions) + * [Focus Timer feature](#focus-timer-feature) + * [Start Session](#start-session) + * [Gamification feature](#gamification-feature) + * [Gain XP and level up](#gain-xp-and-level-up) + * [Saving data](#saving-data) + -## Design & implementation +# Acknowledgements -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +1. Reference to AB-3 Developer Guide +* [Source URL](https://se-education.org/addressbook-level3/DeveloperGuide.html) +* Used as template to structure this DeveloperGuide +2. Reference to AB-3 diagrams code +* [Source URL](https://github.com/se-edu/addressbook-level3/tree/master/docs/diagrams) +* Used as reference to understand PlantUML syntax +# Setting up, getting started + +## Setting up the project in your computer + +Firstly, **fork** this repo, and **clone** the fork into your computer.
+
+If you plan to use Intellij IDEA (highly recommended):
+ +1. **Configure the JDK**: Follow the guide + [[se-edu/guides] IDEA: Configuring the JDK](https://se-education.org/guides/tutorials/intellijJdk.html) + to ensure Intellij is configured to use **JDK 11**. +2. **Import the project as a Gradle project**: Follow the guide + [[se-edu/guides] IDEA: Importing a Gradle project](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) + to import the project into IDEA.
+ **Note**: Importing a Gradle project is slightly different from importing a normal Java project. +3. **Verify the setup:** + 1. Run the ```wellnus.WellNus``` class in IntelliJ and try a few commands. + 2. Run the tests to ensure they all pass. + +## Before writing code + +1. **Configure the coding style**
+ If using IDEA, follow the guide + [[se-edu/guides] IDEA: Configuring the code style](https://se-education.org/guides/tutorials/intellijCodeStyle.html) + to set up IDEA’s coding style to match ours.
+
+2. **Set up CI**
+ This project comes with a GitHub Actions config files (in `.github/workflows` folder). + When GitHub detects those files, it will run the CI for your project automatically at each push + to the `master` branch or to any PR. No set up required.
+
+3. **Learn the design**
+ When you are ready to start coding, we recommend that you look at the class diagrams to understand the structure of + the + code and the interaction among different classes.
+ +# Design & implementation + + + +## Application Lifecycle + +### Overview + +The overall execution lifecycle of the WellNus application involves 4 main components, as shown in the diagram below. + +![Application Lifecycle](diagrams/WellnusSequence.png) + +The application begins with a call to `WellNus.start()`, which initialises an instance of `MainManager` and calls the +`MainManager.runEventDriver()` method. + +`MainManager.runEventDriver()` will then take control of user input and provide a basic interface that parses commands +from the user. This basic interface only supports basic commands such as `help` and `exit` and recognises the keywords +of all supported features in WellNUS++. When a recognised feature keyword is given, the corresponding `FeatureManager` +will be activated through its `runEventDriver()` method, which gains control of user input from `MainManager`. On the +other hand, `MainManager.runEventDriver()` terminates when the `exit` command is given, after which the user exits from +the application. + +After control of user input is granted by `MainManager`, `FeatureManager.runEventDriver()` provides the user with a +feature-specific user interface that continuously parses user commands to determine the suitable `Command` class to +handle any given command. In the case of supported commands besides 'home', the `execute()` method of the corresponding +`Command` class is called to perform a particular action requested by the user. On the other hand, the `home` command +will terminate the `FeatureManager.runEventDriver()` loop, returning the user to the main WellNus++ interface provided +by `MainManager.runEventDriver()`. + +### Rationale + +`WellNus` directly transfers control of user input to `MainManager.runEventDriver()` as managing user input is the +expected functionality of the `runEventDriver()` method within a particular implementation of `Manager`, which means +that conceptually, management of user input belongs in a subclass of `Manager` instead. Besides, this abstraction +of user input logic from `WellNus` fulfils the `Single Responsibility Principle` since `WellNus` is intended +to be a high-level class that delegates tasks to specialised classes that provide the expected functionality, and thus +`WellNus` must not be responsible for concrete logic such as managing user input. + +Additionally, `MainManager.runEventDriver()` is intentionally restricted to only recognise basic commands and feature +keywords to firstly, achieve the encapsulation and abstraction of feature-specific logic from `MainManager`. Moving +feature-specific logic such as recognising feature-specific commands to corresponding feature `Managers` ensures that +actual implementation details in feature-related subpackages are hidden from `MainManager`. This is necessary for +the purpose of encapsulation since `MainManager` exists in a different subpackage. At the same time, by providing the +public `runEventDriver()` in feature `Managers`, `MainManager` is only aware of the expected functionality of the +`runEventDriver()`, which can be used to support feature-specific commands, without being involved in the implementation +details. This allows `MainManager.runEventDriver()` to be kept abstract while providing the expected functionality of +the application. Secondly, this design fulfils the `Single Responsibility Principle` as `MainManager` is solely +responsible for the main WellNUS++ commands but not any feature-specific ones, which means that its logic will only +be changed for reasons related to the main WellNUS++ commands only. + +Lastly, the `runEventDriver()` method of feature `Managers` delegates the execution of commands to implementations of +`Command` to abide by the `Single Responsibility Principle`. Every `Manager.runEventDriver()` method is expected to +provide a particular user interface, but not any commands. This means that this method should only change for reasons +related to its user interface, which requires that command handling logic be implemented elsewhere so that changes in +commands do not require changes in any implementation of `Manager.runEventDriver()`. Besides, this approach ensures +abstraction of logic as `Manager.runEventDriver()` ensures that command handling is performed while avoiding the +actual implementation details by delegating the task to a particular implementation of `Command.execute()`, which is +known to provide command handling functionality. + + + + +## UI Component + +UI component is in charge of reading in user input and printing output. + +### UI Implementation + +![UI Class Diagram](diagrams/UiComponent.png) +The `TextUi` superclass is created for printing standard output and error messages. Each feature has its own UI subclass +which +inherits from `TextUi` to support more customised I/O behaviours.
+Main WellNUS++ uses TextUi
+Atomic Habit uses AtomicHabitUi
+Self Reflection uses ReflectUi
+Focus Timer uses FocusUi
+Gamification uses GamificationUi
+For example, the line separator for Self Reflection is `=` and for Atomic Habit is `~`. + + + + +## Self Reflection Component + +This `Reflection` component provides users with random sets of introspective questions to reflect on, achieving the goal +of improving their wellness.
+ +### Design considerations + +#### User design considerations + +* The sets of questions generated everytime are designed to be randomised to allow users to reflect on different aspects + of their lives. +* Users can review the previous set of questions generated and add questions they resonate well into their favorite list + for review in the future. Similarly, they can also remove questions they no longer resonate from their favorite list + to + ensure the relevancy of the list. +* `help` command and prompting messages are available to guide users in using Self Reflection. For example, an alert + will + be given to users if they `unlike` a question when their favorite list is empty. + +``` +============================================================ + The favorite list is empty, there is nothing to be removed. +============================================================ +``` + +* A unique line separator `=` is used to differentiate Self Reflection from other features and give users a better + visual + indication. + +#### Developer design considerations + +* **Abstracted `QuestionList` Class**
+ Self Reflection section relies heavily on the set of random sets of questions generated and this set will be shared + across different classes. A `QuestionList` class is used to store and manipulate the lists of questions such as the + random sets and the favorite list. A common `QuestionList` object is constructed and passed into different command + object + constructors as an argument. As such, information of lists of questions and their associated methods are centralised + and shared among different objects. +* **Generate random sets and match user input index to real question index**
+ Multiple data structures are used randomise the sets of questions. An **ArrayList** of 10 questions + will be loaded upon launching the program. A **Set** of 5 randomised distinct integers ranging from 0-9 will be + generated. + This **Set** of integers are the used as the index of questions in the **ArrayList** to select the corresponding + questions + and stored for other usages (e.g. `like`, `unlike` commands). + The displayed index of questions increments from 1 to 5, which might differ from their real indexes in the ArrayList. + A **HashMap** is then used with displayed index being the key and real question index being the value to ensure that + the correct + question will be mapped to from user input index (i.e. displayed index). +* **User input validation**
+ Checking mechanism is used to validate user input. The first validation happens at manager level and + the `CommandKeyword` will be checked. + A correct type of command object will be created based on `CommandKeyword`. The second validation happens at command + level + to validate arguments and payloads. This is done at command level instead of manager level as different commands might + have + different requirements for the inputs. + +### Self Reflection Implementation + +![Reflection Component Sequence Diagram](diagrams/ReflectionSequenceDiagram.png) +A `ReflectionManager` object is created by the WellNUS++ `MainManager`. It uses a `ReflectUi` and `CommandParser` object +to constantly reads in and interprets user input and create the correct command for execution based on input +command type until a `HomeCommand`. A common `QuestionList`object is shared among command objects to retrieve and modify +user data. + +![Reflection Component Class Diagram](diagrams/ReflectionClassDiagram.png) +`ReflectionManager` class:
+ +- The main event driver of **Self Reflection** feature. +- It inherits from abstract `Manager` class to standardise behaviours. For example. `ReflectionManager` needs to + override a + standardised abstract method `runEventDriver()` as that this method can be better invoked by the `MainManager`. +- Each `ReflectionManager` object contains exactly one `ReflectUi` object as an attribute to constantly get user inputs. + This is to + use a common `Scanner` object (created in the `ReflectUi` object) to read all the user inputs within Self Reflection + feature. This can avoid potential unexpected behaviours from creating multiple `Scanner` objects. +- The `runEventDriver()` method is the entry of the Self Reflection feature. It contains a **while loop** to + continuously get user input commands as users are expected to continuously perform a series of actions within Self + Reflection + feature until they wish to return back to main WellNUS++ interface(input `home` command). +- Based on the input command type, the `executeCommands()` method will create the correct command objects and + invoke the execution of these commands. Since the command objects are local variables, they are dependencies + for `ReflectionManager` class. + +`QuestionList` class:
+ +- This class stores the list of 10 `ReflectionQuestion` objects available in Self Reflection. It is in charge of + retrieving and modifying + user data related to `ReflectionQuestion` such as the favorite list and the indexes of the previously generated set of + questions. +- A `ReflectionManager` object has exactly one `QuestionList` object which is then passed by reference to construct + command + objects(`LikeCommand`, `GetCommand` etc). Hence, it is a dependency for all command objects in Self Reflection. This + structure + allows data to be centralised and well organised by one class. +- By abstracting the above-mentioned attributes and methods as a separate class instead of putting them + in `ReflectionManager`, the `ReflectionManager` class can solely focus command execution. All the data related to the + list of questions is taken care of by the `QuestionList` class. As such, Single responsibility can be better achieved. +- A `QuestionList` object has exactly one `Storage` and `ReflectionTokenizer` class to store data into data file upon + update + and load data from data file upon launching WellNUS++. + +`ReflectionQuestion` class:
+ +- Each introspective question is a `ReflectionQuestion` object. +- It contains the basic description of the introspective question. Being modelled as an object instead of pure string, + each question will be able to have more attributes which might be utilized for future features. + +`ReflectUi` class:
+ +- This subclass inherits from `TextUi` superclass. It allows Self Reflection feature to have more customised output + behaviour(e.g. type of separators). + +`ReflectionCommands` class:
+ +- This represents a collection of all commands in Self Reflection feature, which will be explained in more detail at + later section. +- Each command class inherits from `Command` abstract class and override `validateComand()` abstract method to validate + command. +- Commands available in Self Reflection:
+ Get a random set of reflection questions: `get`
+ Add a particular question into favorite list: `like INDEX`
+ Remove a particular question from favorite list: `unlike INDEX`
+ View questions in the favorite list: `fav`
+ Review the previous set of questions: `prev`
+ Help command: `help`
+ Return back to main WellNUS++: `home` + +#### Self Reflection commands implementation + +![Reflection Commands Class Diagram](diagrams/ReflectionCommandsUML.png) + +`GetCommand` class:
+ +- Command format: `get` +- This command generates a set of 5 random introspective questions for users to reflect on. +- A `QuestionList` object is passed in as a dependency to provide the pool of 10 introspective questions available + and generate the set of indexes. + +`LikeCommand` class:
+ +- Command format: `like INDEX` +- Users can add reflection question that is generated in the previous set into their favorite list. As there + will only be 5 questions per random set, the indexes are restricted to integer 1~5. +- The `QuestionList` class is used to as a dependency and `addFavQuestion()` method in called to add and store the data. +- Every time a question is added into the favorite list, the indexes of this particular question will be stored in data + file straightaway. It prevents data loss due to unforeseen computer shutdown. +- Users can only successfully add a question to favorite list if they have gotten **at least** one set of questions + previously. + +`UnlikeCommand` class:
+ +- Command format: `unlike INDEX` +- Users can remove reflection questions from their favorite list. +- The `removeFavQuestion()` method in `QuestionList` class is used to remove data and the mechanism is similar to `like` + command. + +`FavoriteCommand` class:
+ +- Command format: `fav` +- Users can review the questions in their favorite list. +- The `getFavQuestions()` method in `QuestionList` class is called to retrieve the questions based on the indexes in the + favorite list. + +`PrevCommand` class:
+ +- Command format: `prev` +- Users can review the set of questions generated by the previous `get` command. It only works if users have gotten + **at least** one set of questions. + +`HelpCommand` class:
+ +- Command format: `help [COMMAND_TO_CHECK]` +- Every command class has public attributes `COMMAND_DESCRIPTION` and `COMMAND_USAGE`. +- `printHelpMessage()` method in `HelpCommand` will retrieve and print these attributes. + +`HomeCommand` class:
+ +- Command format: `home` +- This command allows users to return back to the main WellNUS++ interface. + + + + + +## CommandParser Component + +The CommandParser is a core feature of WellNUS++. +It defines the following: + +1. The syntax for users to input commands +2. A common API for developers to **process** user input + +### Design Considerations + +The CommandParser is implicitly used by users 100% of the time. +It is the abstraction through which the users will interact with WellNUS++'s features. +Its ease of use is critical to ensure a good user experience. + +#### User design Considerations + +Our [target user profile](#target-user-profile) are Computing and Engineering students. +With that, we have done extensive research and laid out the following design considerations. + +1. **Easy learning curve** + Our users are often strapped for time and tend to prefer to use tools that + they are familiar with or can learn quickly. Our command syntax should be easy + to remember, predictable and intuitive. +2. **Flexible usage** + "Arguments" for a command should not care about the order of arguments. + Users often type what comes to mind first. Allowing flexible order of arguments + reduces the cognitive load on the user's end and allows for a + more pleasant experience. + +#### Developer Design Considerations + +Virtually every feature in WellNUS++ will require user input to be processed. This means that all features +will have to interact with `CommandParser`. Hence, the +design for the `CommandParser` API must be understandable, unambiguous and easy to develop on. + +1. **Easy way to extract components of user input** + Each component of userInput (arguments, payload, etc) should be obtainable in predictable and non-arbitrary way. + Arbitrary way (using index) is not preferred as it is prone to developer erros. +2. **Easy way to validate user input** + There should also be built-in ways to easily validate components of user input for a command, + such as checking length. + +#### Alternative Designs Considered + +We considered alternative command structures such as [AB3](https://se-education.org/addressbook-level3/UserGuide.html) +where input types are +specified , `e.g. n/John Doe` which more 'secure' from the get go. +However, due to the following issues, AB3 was not chosen as the alternative solution compared to the shell-like +structure. + +**Steep learning curve** +For experienced and inexperienced users, it is a hassle to remember what letter corresponds to what argument. +For AB3, the user needs to remember all the different `char` 'verbs' such as `e/` for email, `n/` for name. +This violates user design consideration (1). + +**Does not scale well** +AB3 structure runs the high risk of argument-space collision as well. +For example, consider a command that needs an "email" and "entry". What does `e/` correspond to? +We could simply just put entry as *some other character* -- but that defeats the purpose of having the structure in the +first place as the character is the argument's first character. +This makes behaviour **unpredictable** and a **confusing** user experience. + +**Bad expert user experience** + +For expert users and CLI-masters, pedantic argument input like AB3 makes the typing experience MUCH slower due to the +need to type which is relatively clunky as the user will need to type far off to the '/' key on the keyboard. + +### CommandParser Syntax + +The command parser defines any arbitrary user input to be valid +if it follows the following structure. + +``` +mainCommand [payload] [--argument1 [payload1] --argument2 [payload2] ... ] +``` + +This should be familiar to you. It is similar to how most CLI applications process arguments. +In particular, we adapt the structure from `unix` style CLI apps. For example, `git --help`'s +output is shown below. + +![Example](diagrams/git_command.png) +
Example of CLI input syntax, using git as an example
+ +This achieves user design consideration (1). Why? +This syntax is intuitive at a glance to our target users, +is predictable and easy to remember as the only thing they need to remember is the argument name and +the '--' delimiter. + +From this syntax, we can generalise ALL user inputs as `(argument, payload)` pairs. +`mainCommand` is a special `argument`, where it MUST be the first word in the user input. + +Due to the unique one-to-one relationship between arguments and payloads, we can model a user input +using this syntax using a `HashMap` mapping each `argument` to a `payload`. + +For example, +`foo bar --arg1 payload1 payload1--1 --arg2 payload2 --arg3` + +Will be mapped as: +`(foo, bar), (arg1 payload1 payload1--1), (arg2 payload2), (arg3, "")` +where `""` represents an empty string (for visualization). + +Using a `HashMap` fulfils user design considerations (2), both developer design considerations (3), (4). + +- (2): Order of arguments do not matter in a HashMap +- (3): To get a `payload`, the developer simply needs to call `myHashMap.get("argument")`. + This syntatic sugar prevents developer errors compared to an index-based approach. +- (4): Validating commands is much less difficult using `HashMap`. For example, size can be checked with + built-in `.size()`, + argument existence can be queried with `.containsKey()`. + +### Implementation + +#### Integration with WellNUS++ + +![Integration](diagrams/CommandParserClass.png) + +`CommandParser` integrates into the boilerplate via the abstract Manager class. +All features are controlled by a manager subclass - hence the developers just need to call +`getCommandParser` to get a reference to the `CommandParser` taking care of all commands +in the `Manager` subclass. + +#### CommandParser API + +There are only two methods that developers need to know to use `CommandParser`. + +1. `parseUserInput` +2. `getMainArgument` + +**Usage: `parseUserInput`** + +`parseUserInput` is used to get a `HashMap` representation of the user input, a bijection +between `argument` and `payload` pairs. + +Implementation of `parseUserInput`: + +![CommandParser implementation](diagrams/CommandParserSequence.png) + +**Caveats**: For sake of generalization, `FooUi` and `FooManager` are aliases for the actual implemented `TextUi` and +`Manager` subclasses. For example, `AtomicHabit` and `AtomicHabitManager`. + +`parseUserInput(String userInput)` is used to directly convert a string into their argument-payload pairs. +It first calls `splitIntoCommand` to split input over the `' --'` delimiter to get a `String[] commands`. +Each `command` in `commands[]` contain the argument and payload. Internally, it splits the +argument from the payload and populates a `HashMap` with the one-to-one mapping. After all `command`s have +been processed, the map is returned to the `Manager` for usage. + +**Sample Code** + +```java +// Example usage to get the HashMap +public class FooManager extends Manager { + public HashMap handleCommand(String userInput) { + // Get a reference to the parser + CommandParser parser = getCommandParser(); + // Get the one-to-one mapping + HashMap result = parseUserInput(userInput); + return result; + } +} +``` + +**Usage: `getMainCommand(userInput)`** + +To understand what the user wants to do, we need a convenient way to get the `mainCommand` from the user input. +The canonical way to do this is to use `getMainCommand`. This defeats adversarial input where the main command +is input as an argument. + +Internally, this just splits the string by whitespace and returns the first word in the array. + + + +## AtomicHabit Component + +The `AtomicHabit` component is responsible for tracking the user's daily habits +inorder to help users inculcate useful habits. + +It consists commands that allows the user keep track of their habits such as adding, updating and more. + +### Design Considerations + +#### User design considerations + +* The output of the `AtomicHabit` component is designed to be simple and informative + as any changes to their habits will be printed out for the user. +* A variety of commands are provided to allow the user to easily add, update and delete their habits. +* `help` command and prompting messages are available to guide users in using AtomicHabit feature. For example, when + user + inputs `list` and there is no habit in the list, the following message will be printed out: + +``` + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + You have no habits in your list! + Start adding some habits by using 'add'! +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``` + +* A unique line separator `~` is used to differentiate AtomicHabit from other features and give users a better + visual indication. + +#### Developer design considerations + +* **Abstracted `AtomicHabitList` Class**
+ An abstracted `AtomicHabitList` class is created to store the list of habits. This centralised `AtomicHabitList` + object is + passed to other objects.This allows the `AtomicHabit` component to be easily extended to support more features in the + future. +* **Duplicate Checking**
+ Duplicate checking is done to prevent the user from accidentally adding the same habit twice. This is done by checking + if the habit + already exists in the `AtomicHabitList` object. +* **User input validation**
+ Checking mechanism is used to validate user input. User input is always extensively checked + using the `validateCommand()` method in `Command` class before attempting to prepare or execute any command. + +### AtomicHabit Implementation + +![AtomicHabitManager Implementation](diagrams/AtomicHabitSequenceDiagram.png) +An `AtomicHabitManager` object is created by the WellNUS++ `MainManager`and takes over control of the application +when user enters the `AtomicHabit` feature. It uses a `AtomicHabitUi` and `CommandParser` object +to constantly read in and interpret user input and create the correct command for execution based on input +command type until a `HomeCommand`. A common `AtomicHabitList`object is initialised by `AtomicHabitManager` and is +shared among command objects to retrieve and modify user data. + +![AtomicHabitClass](diagrams/AtomicHabit.png) +Note: For readability, AtomicHabitCommand is an abstraction of all the 6 different commands that exist in AtomicHabit. + +#### AtomicHabit Commands + +`AddCommand` class:
+ +- Command format: `add --name ATOMIC_HABIT_NAME` +- Users can add new habit to their habit list to track their progress.
+- `addAtomicHabit()` method in `AtomicHabitList` will add the habit to the habit list. + +`DeleteCommand` class:
+ +- Command format: `delete --id HABIT_INDEX` +- Users can delete habit from their list once they have inculcated the habit. +- `deleteAtomicHabit()` method in `AtomicHabitList` will delete the habit from the habit list. + +`HomeCommand` class:
+ +- Command format: `home` +- This command allows users to return back to the main WellNUS++ interface. + +`ListCommand` class:
+ +- Command format: `list` +- This command allows users to view all the habits they have added. +- ArrayList of 'AtomicHabit' objects is iterated through and the attributes are printed out. + +`UpdateCommand` class:
+ +- Command format: `update --id HABIT_INDEX [--by NUMBER_TO_CHANGE]` +- This command allows users to increment or decrement number of times the habit is done. +- `increaseCount()` and `decreaseCount()` methods in `AtomicHabit` will increment or decrement the habit accordingly. + +`HelpCommand` class:
+ +- Command format: `help [COMMAND_TO_CHECK]` +- Every command class has public attributes `COMMAND_DESCRIPTION` and `COMMAND_USAGE`. +- `printHelpMessage()` method in `HelpCommand` will retrieve and print these attributes. + + + +## Gamification Component + +The Gamification feature is supported by the `GamificationManager` class, which delegates specific application logic to +4 main sets of classes: `Commands`, `GamificationData`, `GamificationStorage` and `GamificationUi`. + +![Gamification Classes](diagrams/GamificationClassDiagram.png)
+ +### Design Considerations + +Logic related to specific tasks such as user commands, XP data, storage and user interface are delegated by +`GamificationManager` to specialised classes to fulfil the `Single Responsibility Principle` since a `Manager` is only +a high-level abstraction that ensures that a feature provides all the expected functionality. Hence, +`GamificationManager` should change only when the overall specification of the gamification feature changes, not when a +particular task changes(e.g. a change in the gamification feature's user interface style). The design considerations for +each of the 4 sets of specialised classes are as follows. + +#### GamificationData + +`GamificationData` encapsulates all useful statistics for the gamification feature and exposes them using helper +methods, thus acting as a compound data type. This is done to increase cohesion since all these useful statistics are +computed from the total XP points data, and thus they are all logically related and can be grouped together in one +class. + +Additionally, `GamificationData` provides a layer of abstraction between the individual statistics and classes +that access them. This greatly simplifies the code of such classes, which only have to call `GamificationData`'s +helper methods. It also fulfils the Object-Oriented Principles of `abstraction` and `encapsulation`. Firstly, +abstraction of logic for computing the statistics makes classes that access the statistics easier to maintain since only +one class, `GamificationData`, needs to be updated to modify the statistics logic. Secondly, abstraction reduces code +duplication since the common logic for computing statistics can be shared between multiple classes through one single +definition of `GamificationData`. Finally, encapsulation ensures that classes from other packages that access the +statistics do not know the implementation details for computing any of the individual statistics, which is necessary +since such classes are from a different package than `GamificationData`. + +#### GamificationStorage + +`GamificationStorage` implements logic for storing `GamificationData` into gamification's data file and provides them +using helper methods. This reduces coupling between `GamificationData` and the actual `Storage` and +`GamificationTokenizer` classes by adding a layer of abstraction. The rationale behind this is the fulfilment of the +`Single Responsibility Principle`: changes in the `Storage` and `GamificationTokenizer` helper methods will not +require updates to `GamificationData`, whose responsibility is to provide gamification statistics, not handle data +storage and retrieval. + +#### GamificationUi + +`GamificationUi` is a child class of `TextUi` and thus provides all of `TextUi`'s functionality while adding +gamification-specific logic such as the XP bar. This design is chosen because the gamification feature requires +`TextUi`'s features, but also needs to customise the format of user messages and introduce additional UI elements. +Extending `TextUi` enables `GamificationUi` to do exactly this, but more importantly, gamification-specific +customisations and logic is abstracted from `TextUi` and put in the `gamification` subpackage. This provides 3 +benefits: + +1. Gamification's customisations will not affect other classes that call `TextUi`'s methods +2. Other classes(from different subpackages) that access `TextUi` are unable to call on gamification-specific methods +3. Fulfilment of the `Open Closed Principle`: The introduction of `GamificationUi` requires no modifications to + `TextUi`'s helper methods, but `GamificationUi` provides additional functionality like an XP bar. + +#### Commands + +The key command for the gamification feature is the `stats` command provided by the `StatsCommand` class. Due to the +implementation of `GamificationData` and `GamificationUi`, `StatsCommand` is a high-level abstraction that delegates +the printing of the XP bar to `GamificationUi`, which obtains the XP statistics to be displayed from the given +`GamificationData`. This greatly simplifies the maintenance of the `StatsCommand` class, which can remain unchanged +even when the logic for computing the XP statistics or the implementation of the XP bar changes. It also reduces +coupling between the `StatsCommand` class and the statistics and UI logic of the gamification feature. + +### Alternative Designs Considered + +#### Defining gamification statistics logic within `GamificationManager` + +Instead of defining a separate `GamificationData` class that is initalised by `GamificationManager`, logic for +computing statistics can be defined within `GamificationManager` itself. This design was discarded because firstly, it +violates the `Single Responsibility Principle`. Implementations of `Manager` are supposed to be high-level abstractions +that delegate tasks to specialised classes. By encapsulating statistics logic within `GamificationManager`, it has to +be updated when the logic for computing the gamification statistics is changed, but the specialised task of computing +statistics is not `GamificationManager`'s responsibility. + +Secondly, defining logic for computing statistics within `GamificationManager` requires passing a reference to +`GamificationManager` to other packages such as atomic habits, which updates the user's total XP. This is unacceptable +since `GamificationManager` has access to all of gamification's state, which other features shouldn't have, so it +cannot be passed by reference to other packages. + +Finally, this design results in high coupling between `GamificationManager` and other classes such as `StatsCommand` +that require access to gamification statistics. This is unideal since `GamificationManager` is intended to be a high +level abstraction, which means it should be loosely coupled with other classes, and high coupling also makes the +maintenance of `GamificationManager` more difficult. + +#### Integrating `GamificationStorage` logic within `GamificationData` + +Logic for calling `Storage` and `GamificationTokenizer` to perform data storage and retrieval can be integrated within +`GamificationData`, eliminating the need for a separate class `GamificationStorage`. This design was rejected because +firstly, it violates the `Single Responsibility Principle`. `GamificationData`'s responsibility is to compute +gamification statistics, not perform data storage and retrieval of any kind. However, this design would necessitate +updating the logic in `GamificationData` whenever the `storage` classes are modified, which contradicts the `Single +Responsibility Principle`. + +Secondly, this design leads to high coupling between `GamificationData` and `storage` classes such as `Storage` and +`GamificationTokenizer`. This makes maintenance more difficult, as changes in the `storage` classes can create a +larger ripple effect as `GamificationData` also has to be updated. + + + + + + +## Tokenizer + +![Tokenizer](diagrams/Tokenizer.png)
+The `Tokenizer` interface is the superclass for classes responsible for converting data stored temporarily in feature's +Managers into Strings for storage and also convert Strings from storage back into data that can be restored by Managers. + +### Design Considerations + +Each `Tokenizer` provides `tokenize()` and `detokenize()`, which can then be adapted for each feature. This fulfills the +`Single Responsibility Principle` as each `Tokenizer` are only responsible to tokenize and detokenize data from only one +Feature. Furthermore, this design also fulfills `Open-Closed Principle` where `Tokenizer` interface are open for +extension +should there be a new feature added into WellNUS++., while the `Tokenizer` feature itself are closed for modification. +In +addition, this design principle fulfills the `Dependency Inversion Principle` as the feature's Managers are not +dependent on +actual implementation of `Tokenizer`, but on the abstract of `Tokenizer` class and its `tokenize()` and `detokenize()` +method. Each feature's tokenizer are free to implement `tokenize()` and `detokenize()` as every feature might store +different +kinds of data. + +### Individual Tokenizers + +`AtomicHabitTokenizer` class is responsible to tokenize and detokenize ArrayList of AtomicHabits that +AtomicHabitManager will +use or store. Each habit will be tokenized in the following +format `--description [description of habit] --count [count of +habit]` using the `tokenize()` method. While `detokenize()` method converts the strings back to ArrayList of AtomicHabit +that +can be initialized in AtomicHabitManager to restore the state of the Manager. + +`ReflectionTokenizer` class is responsible to tokenize the liked question's index and previous questions's index and +detokenize +it back. ArrayList of Set containing the index of `like` and `pref` will be passed to the `tokenize()` function. The +data will +be stored in the following format + +``` + +like:[index of liked question] +prev:[index of previous question] + +``` + +`detokenize()` then can be called by ReflectionManager to retrieve the ArrayList containing the Set of liked and +previous +questions' index to restore its state. + + + + +## Storage + +Storage is a common API built to work completely decoupled from any `Tokenizer` implementation. + +It comes with two methods that developers need to be aware of to save and load data: + +- `saveData(ArrayList tokenizedManager, String fileName)` +- `loadData(String fileName)` + +### Usage - `saveData` + +To illustrate the overall flow on how to save data, refer to the sequence diagram below. +Saving: `saveData`, `Storage` allows for any tokenizing structure logic as long as the input data is in the form +of an `ArrayList`. + +The general idea is to `tokenize` it first into the `ArrayList` format calling before +calling `Storage`'s `saveData` method. + +`FooTokenizer` and `FooManager` are named as such to generalize the features that use `Storage`. `` is also used to +generalize the data structure that is being +passed into a feature-specific tokenizer, such as `AtomicHabit`. + +The burden of data transformation from the target data type to `String` is up to `Tokenizer`'s `tokenize` method. + +![](./diagrams/StorageSequence-Saving_Data__Emphasis_on_Storage_Subroutine_.png) +**Caveats**: For sake of generalization, `FooManager` and `FooTokenizer` are aliases for the actual implemented +`Manager` and `Tokenizer` subclasses. For example, `AtomicHabitManager` and `AtomicHabitTokenizer`. + +### Usage - `loadData` + +`loadData` works similarly to `saveData`, but with the logic reversed. + +`loadData` will load all `WellNUS++` data into a common data type, `ArrayList`. +The string list can then be use wholesale or detokenized into an appropriate data structure. + +The burden of data transformation from `String` to the target data type to is up to `Tokenizer`'s `detokenize` method. + +### Design Considerations + +- Only filenames defined by public string constants in the `Storage` class. + This is meant to prevent developer mis-use and control what exactly waht files WellNUS++ can create. +- Internally, each entry in `ArrayList` will be delimited by ` --\n`, where \n is `System.LineSeparator()`. + This was chosen due to the invariant property of `' --'` in the context of WellNUS++. Due to the way all user input + is filtered by the `CommandParser`, the chosen delimiter should never show up in any data input, such as a habit name + from `AtomicHabits`. + + + +## Focus Timer Component + +The `Focus Timer` component is responsible for tracking the user's daily habits. +It consists of the `feature` package and the `command` package. + +It contains commands that you would expect from a timer, such as stopping, +pausing, and more. + +### Focus Timer Implementation + +The focus timer contains a `FocusManager`. +The session is a wrapper for all the `Countdown` and contains utility logic to identify state and manage Countdown. +`Countdown` houses the timer that actually does the counting and holds attributes that help +identify the state of the FocusTimer. + +![FSM diagram](diagrams/FocusTimerClassDiagram.png) + +**Caveats**: For readability, FocusCommand is an abstraction of all the 9 different commands that exist in FocusTimer. + +#### State Management + +The timer is an inherently complex feature. There are many commands, and some commands +logically cannot be executed in certain states. For example, if the timer is `Paused`, +the user cannot go to the `next` Countdown. + +**Problem**: It is confusing to developers to check if the `command` that they are writing + +**Solution**: To help developers, we define the expected behaviour for focus timer +in this **simplified** finite state machine (FSM) diagram. + +The black circle represents the entrypoint into FocusTimer, and +the labels of the arrows are the valid `command`. +The command `home` has been left out to make the diagram simpler. +It is a command that can be called in any state, and therefore does not add value to it. + +![FSM diagram](diagrams/FocusTimerState.png) + +From the diagram and the class diagram, we can derive a truth table +from the attributes of each Countdown (e.g. `isReady`) and tag them to a state. + +| ∨ State / Attribute > | isRunClock | isCompletedCountDown | isReady | +|--------------------------|------------|----------------------|---------| +| Ready | X | X | T | +| Counting | T | F | F | +| Waiting | F | T | F | +| Paused | F | F | F | + +Truth table, where X denotes a 'dont care' condition +where the truth value does not matter. + +From this, we can easily check which state we are in and execute/not execute commands as necessary. +Referring to the class diagram, this is implemented on `Session` with 4 methods that help identify the state: + +Example implementation to check if Session is in `Counting` state: + +```java +public boolean isSessionCounting() { + Countdown countdown = getCurrentCountdown(); + return countdown.getIsRunning() && !countdown.getIsCompletedCountdown(); +} +``` + +**Easily Identify State**: Developers can hence trivially check if a command is in a valid state to be executed +by using these 4 methods in `Session` to check which state that it is in: +1. `isSessionReady()` +2. `isSessionCounting()` +3. `isSessionWaiting()` +4. `isSessionPaused()` + + + +#### Commands + +`StartCommand` class:
+ +- Command format: `start` +- Users can start the focus session and the first work countdown will begin. +- `startTimer()` method in `Session` will begin the countdown. + +`CheckCommand` class:
+ +- Command format: `check` +- Users can check the time remaining in the current countdown.
+- `getMinutes()` and `getSeconds()` method in `Countdown` is used to retrieve the current time remaining. + +`PauseCommand` class:
+ +- Command format: `pause` +- Users can pause the current timer if they wish to perform other tasks.
+- `setPause()` method in `Countdown` is used to pause the current timer by setting the atomic boolean `isRunClock` to + false. + +`ResumeCommand` class:
+ +- Command format: `resume` +- Users can resume the current timer if they are ready to continue focusing.
+- `setStart()` method in `Countdown` is used to resume the current timer by setting the atomic boolean `isRunClock` to + true. + +`NextCommand` class:
+ +- Command format: `next` +- Users can proceed to the next work or break countdown.
+- `startTimer()` method in `Session` is used to start the next countdown. + +`StopCommand` class:
+ +- Command format: `stop` +- Users can add new habit to their habit list to track their progress.
+- `addAtomicHabit()` method in `AtomicHabitList` will add the habit to the habit list. + +`ConfigCommand` class:
+ +- Command format: `config [--cycle NUM_OF_CYCLE --work WORK_TIME --break BREAK_TIME --longbreak LONG_BREAK_TIME]` +- Users can modify their session to their liking by setting + their own number of cycles, and length of the different timers.
+- `setWork()`, `setBrk()`, `setLongBrk()`, and `setCycle()` method in `Session` is used to update the new values into + `work`, `brk`, `longBrk` and `cycle` attributes. + +`HelpCommand` class:
+ +- Command format: `help [COMMAND_TO_CHECK]` +- Every command class has public attributes `COMMAND_DESCRIPTION` and `COMMAND_USAGE`. +- `printHelpMessage()` method in `HelpCommand` will retrieve and print these attributes. + +`HomeCommand` class:
+ +- Command format: `home` +- This command allows users to return back to the main WellNUS++ interface. + + + +# Appendix - Requirements ## Product scope + +### Product Name + +**WellNUS++** + ### Target user profile -{Describe the target user profile} +* NUS Computing and Engineering students +* Spend lots of time coding on their IDE and type relatively fast +* Have to regularly use digital gadgets and Internet for their courses +* Very familiar with command line interfaces +* Stressed about academy and many others +* Busy with work and drowning in deadlines +* Wants to improve their wellness +* Sometimes unmotivated with short attention span ### Value proposition -{Describe the value proposition: what problem does it solve?} +NUS Computing and Engineering students are often busy with work and sometimes will neglect their wellness. This app aims +to help NUS Computing and Engineering students improve their overall wellness by encouraging the **cultivation of +meaningful +atomic habits**, **practice of self reflection** and **usage of offline timer to stay focused**. By using this app, +we hope users will be more aware of the healthiness of their daily life and take actions to improve their wellness.
+
+WellNUS++ is a CLI app, primarily due to the following reasons: + +* Computing students generally type fast and prefer typing to mouse due to their daily coding routines. +* Due to the data heavy nature and personalised user input of this app, typing will be preferred to clicking. +* In particular, our application is built to reduce context switching. Users can launch the application from the comfort + of their favourite IDE’s terminal to reduce disruption to their daily coding lives. +* Instead of using electronics with fancy GUI, this CLI app gives computing students an opportunity to minimise digital + interaction which will be beneficial for their wellness. +* The app is gamified to make it more attractive for students to use. Levels and micro-goals incentivise our + users to keep using the app’s features, allowing them to focus on their work and achieve wellness. ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|----------------------------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------| +| v1.0 | Computing student who prefers typing over clicking | I can use keyboard instead of mouse | I can use the app efficiently | +| v1.0 | Computing student who is too used to the Internet | Reduce my browsing and information overload | I can improve my attention span | +| v1.0 | Reflective student | I can get one introspective question on-demand | I can reflect and grow emotionally at my own pace | +| v1.0 | Computing student who wishes to improve lifestyle | I can add an atomic habit to track | I can start the process of inculcating a new habit | +| v1.0 | Computing student who wishes to improve lifestyle | I can view all my atomic habits | I can keep track of my self-improvement progress | +| v1.0 | Computing student who wishes to improve lifestyle | I can update my atomic habits count with a positive number | I can adjust the habits based on my progress | +| v1.0 | A new user | I wish to get guidance on how to navigate through the application | I can use this application better | +| v2.0 | Reflective student | I can like introspective questions and view them | I can reflect using my favourite questions | +| v2.0 | Reflective student | I can get the previous questions I viewed | I can re-view these questions | +| v2.0 | Easily distracted computing student | I want to start a timer to keep track of time spent on work | I can do timed-practice | +| v2.0 | Easily distracted computing student | I want to check the time | I can keep track of my pace | +| v2.0 | A regular WellNUS++ user | I wish to have my information stored in the application | I can re-view my past data | +| v2.0 | A busy Computing student | I wish to be able to pause/stop the timer | I can attend to urgent matters during a study session | +| v2.0 | A student with a flexible studying timing | I wish to be able to change my work-break duration | I can use the timer that can fit my schedule well | +| v2.0 | A unmotivated student | I wish to see my achievement and progress in my habits | I can be more motivated in my task and have a goal to work towards | +| v2.1 | A careless student | I wish to decrement my habit counts | I can maintain the correctness of my habit data | +| v2.1 | A careless student | I wish to delete wrongly added atomic habits | I can keep the correct atomic habits in the list | +| v2.1 | A careless student | I wish to remove wrongly added questions in favorite list | I can keep only questions I like in the favorite list | +| v2.1 | A Computing student used to CLI applications | I wish to see a cursor on the screen | I can get accustomed to the application | ## Non-Functional Requirements -{Give non-functional requirements} +1. Should work on any mainstream OS as long as it has Java 11 or above installed. +2. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should + be able to accomplish most of the tasks faster using commands than using the mouse. +3. A user + + ## Glossary * *glossary item* - Definition +* **Mainstream OS**: Windows, Linux, Unix, macOS +* **Main Command**: The first WORD that a user types in. `e.g. reflect, exit` +* **Argument**: A word that is a parameter to a `Main Command` and is prefixed by ` --`. `e.g. --id, --name` +* **Payload**: An (optional) arbitrary sequence of characters immediately following a main command or argument. + The payload will terminate when the user clicks `enter` or separates the payload with another argument + with the `--` delimiter. + + +# Appendix - Instructions for manual testing + +## Launch + +1. Ensure you have Java 11 or above installed in your Computer. +2. Download the latest `[CS2113-T12-4][WellNUS++].jar` from [here](https://github.com/AY2223S2-CS2113-T12-4/tp/releases/latest). +3. Copy the file to the folder you want to use as the home folder for your WellNUS++. +4. Open a command terminal, cd into the folder you put the `[CS2113-T12-4][WellNUS++].jar` file in, and use the + `java -jar [CS2113-T12-4][WellNUS++].jar` command to run the application. A CLI should appear in a few seconds. + +## Sample test cases + + +### Help command + +1. Make sure you are in the main interface, but individual features(i.e. hb, reflect and timer) +2. Test case: `help`
+ Expected output: A list of commands with their usage + Example: + + ``` + + ------------------------------------------------------------ + WellNUS++ is a Command Line Interface (CLI) app for you to keep track, manage and improve your physical and mental wellness. + Input `help` to see all available commands. + Input `help [command-to-check]` to get usage help for a specific command. + Here are all the commands available for you! + + 1. hb - Atomic Habits - Track and manage your habits with our suite of tools to help you grow and nurture a better you! + 2. reflect - Self Reflection - Take some time to pause and reflect with our specially curated list of questions and reflection management tools. + 3. ft - Focus Timer - Set a configurable 'Pomodoro' timer with work and rest cycles to keep yourself focused and productive! + 4. gamif - Gamification - Gamification gives you the motivation to continue improving your wellness by rewarding you for your efforts! + 5. exit - Close WellNUS++ and return to your terminal. + 6. help - Get help on what commands can be used in WellNUS++. + ------------------------------------------------------------ + + ``` + +3. Test case: `help me`
+ Expected output: The list of commands will not be generated as it is an invalid command
+ Example: + + ``` + + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + Error Message: + Invalid payload given to 'help'! + Note: + help command usage: help [command-to-check] + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + + ``` + +4. To get a list of available commands, any command other than `help` is invalid + + + +### Atomic habits feature +#### Add atomic habits + +1. Make sure you are inside **Atomic habit** feature by enter `hb` command after the launch of the program +2. Test case: `add --name make bed every morning`
+ Expected output: A new atomic habit is successfully added
+ Example: + + ``` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Yay! You have added a new habit: + 'make bed every morning' was successfully added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``` + +3. Test case: `add name make bed every morning`
+ Expected output: The atomic habit will not be added in as this is an invalid command
+ Example: + + ``` + + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + Error Message: + Invalid arguments given to 'add'! + Note: + add command usage: add --name (your habit name) + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + + ``` + +4. Any commands that does not follow the format of `add --name ATOMIC_HABIT_NAME` is invalid + + + +### Reflection feature +#### Get reflection questions + +1. Make sure you are inside **Self Reflection** feature by enter `reflect` command after the launch of the program +2. Test case: `get`
+ Expected output: Get a set of 5 random introspective questions
+ Example: + + ``` + + ============================================================ + 1.What is my purpose in life? + 2.What is my personality type? + 3.Did I make time for myself this week? + 4.What scares me the most right now? + 5.What is something that brings me joy? + ============================================================ + + ``` + +3. Test case: `get reflect`
+ Expected output: Introspective questions will not be generated as this is an invalid command
+ Example: + + ``` + + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + Error Message: + Invalid payload given to 'get'! + Note: + get command usage: get + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + + ``` + +4. Any command other than `get` is invalid + +#### Favorite reflection questions +1. Make sure you are inside **Self Reflection** feature by enter `reflect` command after the launch of the program +2. Test case: `fav`
+ Expected output: Empty favorite reflection questions list is printed
+ Example: + + ``` + + ============================================================ + There is nothing in favorite list, please get reflection questions first! + ============================================================ + + ``` + +3. Test case: `get`
+ Expected output: Get a set of 5 random introspective questions
+ Example: + + ``` + + ============================================================ + 1.What is my purpose in life? + 2.What is my personality type? + 3.Did I make time for myself this week? + 4.What scares me the most right now? + 5.What is something that brings me joy? + ============================================================ + + ``` + +4. Test case: `like 3`
+ Expected output: "Did I make time for myself this week?" is added to your favorite reflection questions list
+ Example: + + ``` + + ============================================================ + You have added question: "Did I make time for myself this week?" Into favorite list!! + ============================================================ + + ``` + +5. Test case: `fav`
+ Expected output: "Did I make time for myself this week?" is listed as a favorite reflection question
+ Example: + + ``` + + ============================================================ + 1.Did I make time for myself this week? + ============================================================ + + ``` + + +### Focus Timer feature +#### Start Session + +1. Make sure you are inside **Focus Timer** feature by enter `ft` command after the launch of the program +2. Test case: `start`
+ Expected output: Session begins and work timer counts down
+ Example: + + ``` + + ************************************************************ + Your session has started. All the best! + ************************************************************ + ************************************************************ + Task Cycle: Do your task now! + ************************************************************ + + ``` + +3. Test case: `check`
+ Expected output: Time left in current timer will be printed
+ Example: + + ``` + + ************************************************************ + Time left: 0:27 + ************************************************************ + + ``` + +4. Test case: `pause`
+ Expected output: Timer will be paused and time left will be printed
+ Example: + + ``` + + ************************************************************ + Timer paused at: 0:23 + ************************************************************ + + ``` + +5. Test case: `resume`
+ Expected output: Timer will resume counting down and time left will be printed
+ Example: + + ``` + + ************************************************************ + Timer resumed at: 0:23 + ************************************************************ + + ``` + +6. Test case: `stop`
+ Expected output: Focus session will end and all timers will stop
+ Example: + + ``` + + ************************************************************ + Your focus session has ended. + To start a new session, `start` it up! + You can also configure the session to your liking with `config`! + ************************************************************ + + ``` + + + +### Gamification feature +#### Gain XP and level up +1. Make sure you are inside **Gamification** feature by enter `gamif` command after the launch of the program +2. Test case: `stats`
+ Expected output: Default XP points and XP level is printed
+ Example: + + ``` + + ###################################################################### + # Current XP: Level 0 [> ] # + # 10 more XP to Level 1 # + ###################################################################### + + ``` + +3. Test case: `home`
+ Expected output: Goodbye message of gamification feature is printed
+ Example: + + ``` + + ###################################################################### + # Thank you for using the gamification feature! Return anytime # + ###################################################################### + + ``` + +4. Test case: `hb`
+ Expected output: Enters the atomic habit feature
+ Example: + + ``` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + _ _ _ _ _ _ _ _ + /_\| |_ ___ _ __ (_)__ | || |__ _| |__(_) |_ ___ + / _ \ _/ _ \ ' \| / _| | __ / _` | '_ \ | _(_-< + /_/ \_\__\___/_|_|_|_\__| |_||_\__,_|_.__/_|\__/__/ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Welcome to WellNUS++ Atomic Habits section! + Track and inculcate good habits into your life with us! + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``` + +5. Test case: `add --name My First Habit`
+ Expected output: Adds a new atomic habit called "My First Habit"
+ Example: + + ``` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Yay! You have added a new habit: + 'My First Habit' was successfully added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``` + +6. Test case: `update --id 1 --by 10`
+ Expected output: Completes the "My First Habit" 10 times, which leads to levelling up
+ Example: + + ``` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + The following habit has been incremented! Keep up the good work! + 1.My First Habit [10] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ###################################################################### + # Congratulations! Level up # + ###################################################################### + + ``` + +7. Test case: `home`
+ Expected output: Returns to main `WellNUS++` session
+ Example: + + ``` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Thank you for using atomic habits. Do not forget about me! + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``` + +8. Test case: `gamif`
+ Expected output: Enters the gamification feature
+ Example: + + ``` + + ###################################################################### + Welcome to + ______ _ _____ __ _ + / ____/___ _____ ___ (_) __(_)________ _/ /_(_)___ ____ + / / __/ __ `/ __ `__ \/ / /_/ / ___/ __ `/ __/ / __ \/ __ \ + / /_/ / /_/ / / / / / / / __/ / /__/ /_/ / /_/ / /_/ / / / / + \____/\__,_/_/ /_/ /_/_/_/ /_/\___/\__,_/\__/_/\____/_/ /_/ + ###################################################################### + + ``` + +9. Test case: `stats`
+ Expected output: Prints the latest XP points and XP level
+ Example: + + ``` + + ###################################################################### + # Current XP: Level 1 [> ] # + # 10 more XP to Level 2 # + ###################################################################### + + ``` + + + +## Saving data + +1. Dealing with missing data files + + Ensure data files are created: Add a new atomic habit using the `add --name Test data file` command in the `hb` + session
+ Quit `WellNUS++`: Issue `home` command in the `hb` session followed by `exit` command in the `main` session
+ Delete data files: Delete the `data` folder created in the same folder as the `WellNUS++` jar file you just executed
+ Relaunch `WellNUS++`: Run the `WellNUS++` jar file, issue `hb` command and then issue `list` command. Verify that no + atomic habits are now recorded, i.e. `WellNUS++` should output:
+ + ``` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + You have no habits in your list! + Start adding some habits by using 'add'! + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``` + +2. Dealing with corrupted data files + + Quit `WellNUS++`
+ Open the `data/habit.txt` file located in the same directory as the `WellNUS++` jar file
+ Replace the contents of the `habit.txt` file with the following lines:
+ + ``` + + --description Valid atomic habit --count 1 -- + --corrupted Data --test to be ignored -- + + ``` + + Run the `WellNUS++` jar file. You should see the warning below after the `WellNUS++` greeting logo and message:
+ + ``` + + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + Error Message: + Invalid habit data '--corrupted Data --test to be ignored' found in storage! + Note: + Previous atomic habit data will not be restored. + !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! + + ``` + + View the saved atomic habits: Issue `hb` followed by `list`. Expected output should be:
+ + ``` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + You have no habits in your list! + Start adding some habits by using 'add'! + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``` -## Instructions for manual testing + Explanation: Upon relaunch, `WellNUS++` detected the invalid line `--corrupted Data --test to be ignored --`(hence + the warning message) and cleaned the contents of the data file, leaving no atomic habits recorded -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} + diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..81a0dd4d93 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,8 @@ -# Duke +# WellNUS++ -{Give product intro here} +WellNUS++ is a Command Line Interface(CLI) app for NUS Computing students to keep track and improve their physical and +mental wellness in various aspects. If you can type fast, WellNUS++ can update their wellness progress faster than +traditional Graphical User Interface(GUI) apps. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..059522a53c 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,1149 @@ -# User Guide +# WellNUS++ User Guide -## Introduction +``` +,--. ,--. ,--.,--.,--. ,--.,--. ,--. ,---. | | | | +| | | | ,---. | || || ,'.| || | | |' .-',---| |---.,---| |---. +| |.'.| || .-. :| || || |' ' || | | |`. `-.'---| |---''---| |---' +| ,'. |\\ --.| || || | ` |' '-' '.-' | | | | | +'--' '--' `----'`--'`--'`--' `--' `-----' `-----' `--' `--' +``` -{Give a product intro} +# Introduction -## Quick Start +WellNUS++ is a **Command Line Interface(CLI) app** for **NUS Computing students** to keep track of and improve their +**physical and mental wellness** in various aspects. If you can type fast, WellNUS++ can update your wellness progress +faster than traditional Graphical User Interface(GUI) apps. -{Give steps to get started quickly} +# Table of Contents -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). + -## Features +* [WellNUS++ User Guide](#wellnus-user-guide) +* [Introduction](#introduction) +* [Table of Contents](#table-of-contents) +* [Quick Start](#quick-start) +* [Overview of WellNUS++](#overview-of-wellnus) +* [Features](#features) + * [Command Format](#command-format) + * [`help` - Viewing WellNUS++ Help](#help---viewing-wellnus-help) + * [Accessing feature from Main](#accessing-feature-from-main) + * [`hb` - Accessing atomic habit feature](#hb---accessing-atomic-habit-feature) + * [`add` - Add new atomic habit](#add---add-new-atomic-habit) + * [`list` - List all atomic habit](#list---list-all-atomic-habit) + * [`update` - Update an atomic habit](#update---update-an-atomic-habit) + * [`delete` - Delete an atomic habit](#delete---delete-an-atomic-habit) + * [`help` - Viewing Atomic Habit Help](#help---viewing-atomic-habit-help) + * [`gamif` - Accessing gamification feature](#gamif---accessing-gamification-feature) + * [`stats` - Gamification statistics](#stats---gamification-statistics) + * [`help` - Viewing Gamification Help](#help---viewing-gamification-help) + * [`reflect` - Accessing self reflection feature](#reflect---accessing-self-reflection-feature) + * [`get` - Get reflection questions](#get---get-reflection-questions) + * [`like` - Add reflection question into favorite list](#like---add-reflection-question-into-favorite-list) + * [`fav` - View favorite list](#fav---view-favorite-list) + * [`unlike` - Remove questions from favorite list](#unlike---remove-questions-from-favorite-list) + * [`prev` - Get the previous set of reflection questions generated](#prev---get-the-previous-set-of-reflection-questions-generated) + * [`help` - Viewing Reflection Help](#help---viewing-reflection-help) + * [`ft` - Accessing Focus Timer Feature](#ft---accessing-focus-timer-feature) + * [`start` - Start Session](#start---start-session) + * [`pause` - Pause session](#pause---pause-session) + * [`resume` - Resume session](#resume---resume-session) + * [`check` - Check time](#check---check-time) + * [`next` - Next timer](#next---next-timer) + * [`stop` - Stop session](#stop---stop-session) + * [`help` - Viewing Focus Timer help](#help---viewing-focus-timer-help) + * [`config` - Configure the Timer](#config---configure-the-timer) + * [`home` - Return back main WellNUS++](#home---return-back-main-wellnus) + * [`exit` - Exit WellNUS++](#exit---exit-wellnus) + * [FAQ](#faq) + * [Command Summary](#command-summary) -{Give detailed description of each feature} + -### Adding a todo: `todo` -Adds a new item to the list of todo items. +# Quick Start -Format: `todo n/TODO_NAME d/DEADLINE` +1. Ensure you have Java 11 or above installed in your Computer. -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +2. Download the latest [CS2113-T12-4][WellNUS++].jar + from [here](https://github.com/AY2223S2-CS2113-T12-4/tp/releases/latest). -Example of usage: +3. Copy the file to the folder you want to use as the home folder for your WellNUS++. -`todo n/Write the rest of the User Guide d/next week` +4. Open a command terminal, cd into the folder you put the .jar file in, and run the command + `java -jar "[CS2113-T12-4][WellNUS++].jar"` + to run the application. A CLI should appear in a few seconds (shown below). -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +``` +------------------------------------------------------------ + Very good day to you! Welcome to + + ,--. ,--. ,--.,--.,--. ,--.,--. ,--. ,---. | | | | + | | | | ,---. | || || ,'.| || | | |' .-',---| |---.,---| |---. + | |.'.| || .-. :| || || |' ' || | | |`. `-.'---| |---''---| |---' + | ,'. |\ --.| || || | ` |' '-' '.-' | | | | | + '--' '--' `----'`--'`--'`--' `--' `-----' `-----' `--' `--' +------------------------------------------------------------ +------------------------------------------------------------ + Enter a command to start using WellNUS++! Try 'help' if you're new, or just unsure. +------------------------------------------------------------ +``` +You are now in the `main` session of `WellNUS+`. Access our features by issuing a feature command(described in later +sections). Issue the `help` command to see the list of feature commands available. + +# Overview of WellNUS++ + +WellNUS++ comes with a variety of features to help you enhance your overall wellness in NUS! The features are Atomic +Habit, Self Reflection, Focus Timer and Gamification. + +![WellNUS++ Structure Overview](diagrams/WellNusStructure.png)
+ +Upon running our application jar, users first start in the `main` session. From here, users can navigate to different +features using the commands `hb`, `ft`, `reflect` and `gamif`, which takes them to the feature-specific session(see +later sections for details about each feature session). + +Each feature session provides its own set of commands for users to explore. Return to the `main` session +using the `home` command. + +Issue `help` in any session to find out what commands are available to you. + +Exit `WellNUS++` by issuing the `exit` command(**only** available in the `main` session). + +# Features + + + +## Command Format + +A command has the general structure: + +``` +mainCommand PAYLOAD_0 --argument1 PAYLOAD_1 --argument2 PAYLOAD_2 +``` + +Command syntax: + +* Words in **UPPER_CASE** are the payloads to be supplied by the user. + e.g. in `add --name NAME`, `NAME` is a parameter which can be used in this way: `add --name John Doe` +* Items in square brackets are **optional**. + e.g in `add --name NAME [--tag TAG]`, the command can be used in two ways: + * `add --name John Doe --tag friend`, or + * `add --name John Doe` + * Payloads may also be optional if they are wrapped in `[]` +* Arguments can be in any order. + e.g. Consider `--name NAME --phone PHONE_NUMBER`. + `--phone PHONE_NUMBER --name NAME` is a valid set of arguments. +* The `mainCommand` and `argument` are case insensitive. + e.g. `aDD --nAmE NAME` is equivalent to `add --name NAME` + + + +## `help` - Viewing WellNUS++ Help + +Lists all commands available and provide a short description of the application. + +Format: `help [COMMAND_TO_CHECK]` + +* List all commands available in the app and a short description of the app(`help` with no arguments given) +* Give a detailed explanation of the parameters and subcommands for the given command `COMMAND_TO_CHECK` + +Example of usage 1: + +`help` + +Expected outcome: + +``` +------------------------------------------------------------ + WellNUS++ is a Command Line Interface (CLI) app for you to keep track, manage and improve your physical and mental wellness. + Input `help` to see all available commands. + Input `help [command-to-check]` to get usage help for a specific command. + Here are all the commands available for you! + + 1. hb - Atomic Habits - Track and manage your habits with our suite of tools to help you grow and nurture a better you! + 2. reflect - Self Reflection - Take some time to pause and reflect with our specially curated list of questions and reflection management tools. + 3. ft - Focus Timer - Set a configurable 'Pomodoro' timer with work and rest cycles to keep yourself focused and productive! + 4. gamif - Gamification - Gamification gives you the motivation to continue improving your wellness by rewarding you for your efforts! + 5. exit - Close WellNUS++ and return to your terminal. + 6. help - Get help on what commands can be used in WellNUS++. +------------------------------------------------------------ +``` + +Example of usage 2: + +`help hb` + +Expected outcome: + +``` +------------------------------------------------------------ + hb - Atomic Habits - Track and manage your habits with our suite of tools to help you grow and nurture a better you! + usage: hb +------------------------------------------------------------ +``` +Other feature-specific `help` commands and their expected outputs can be found in the respective sections below. + + +## Accessing feature from Main + +Access specific feature from main interface by inputting the `FEATURE_NAME`.
+ +Feature name can be referenced by calling the help command. + +Take note that users are only allowed to access features from the main session (i.e. `hb`, `reflect`, `ft` and `gamif` +are +only recognised in the main WellNUS++ session, cross feature transition is **not +allowed**). Cross feature transition is banned to ensure that users are able to focus on their +current feature for their own well-being. + +Format: `FEATURE_NAME` + +* Accesses the feature `FEATURE_NAME` to utilise its respective actions + +Example of usage: + +`reflect` + +Expected outcome: + +``` +============================================================ + ##### ###### + # # ###### # ###### # # ###### ###### # ###### #### ##### + # # # # # # # # # # # # # + ##### ##### # ##### ###### ##### ##### # ##### # # + # # # # # # # # # # # # + # # # # # # # # # # # # # # + ##### ###### ###### # # # ###### # ###### ###### #### # +============================================================ + Welcome to WellNUS++ Self Reflection section :D + Feel very occupied and cannot find time to self reflect? + No worries, this section will give you the opportunity to reflect and improve on yourself!! +============================================================ +``` + +The expected interface of the other feature's `FEATURE_NAME` commands can be found in their respective sections below. + + + +## `hb` - Accessing atomic habit feature + +Atomic habit feature allows users to keep track of the daily habits they wish to develop for better self improvement. + +Format: `hb`
+ +Example of usage:
+ +`hb` + +Expected outcome: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + _ _ _ _ _ _ _ _ + /_\| |_ ___ _ __ (_)__ | || |__ _| |__(_) |_ ___ + / _ \ _/ _ \ ' \| / _| | __ / _` | '_ \ | _(_-< + /_/ \_\__\___/_|_|_|_\__| |_||_\__,_|_.__/_|\__/__/ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Welcome to WellNUS++ Atomic Habits section! + Track and inculcate good habits into your life with us! +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +### `add` - Add new atomic habit + +Adds an atomic habit to be tracked by WellNUS++ when accessing atomic habit feature. +Count of the habit will be initialised to 0. + +Format: `add --name ATOMIC_HABIT_NAME` + +* `ATOMIC_HABIT_NAME` is used to uniquely identify each habit(unique and not null). This means habits with + the same `ATOMIC_HABIT_NAME` are not allowed, and a duplicate habit with the same `ATOMIC_HABIT_NAME` + cannot be added later. + +Example of usage: + +`add --name make bed every morning` + +Expected outcome: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Yay! You have added a new habit: + 'make bed every morning' was successfully added +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +### `list` - List all atomic habit + +Shows a list of all atomic habits. + +Format: `list` + +Example of usage: + +`list` + +Expected outcome: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Here is the current accumulation of your atomic habits! + Keep up the good work and you will develop a helpful habit in no time + 1.Make Bed every morning [0] + 2.Read for at least 30 minutes every day [3] + 3.Avoid checking phone for the first hour after waking up [2] + ... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +### `update` - Update an atomic habit + +Increment the number of times that an atomic habit has been carried out.
+Decrement the atomic habits if you wrongly incremented. + +Format: `update --id HABIT_INDEX [--by NUMBER_TO_CHANGE]` + +* (**Optional**) Step 1: You are _recommended_ to list the current habits using the command `list` +* Step 2: Select the habit to update by entering the index number of the habit `HABIT_INDEX` according to index of the + list output.
+ The user can specify the number to change for the habit count via `NUMBER_TO_CHANGE`.
+ The **default** behaviour is to increment the behaviour by 1.
To decrement the habit count, enter a negative + number + instead(see 'Example of usage 2' below). + +Note: '+' in front of `NUMBER_TO_CHANGE` parameter is not necessary when incrementing. +For example, to increment index 1 by 2 counts, issue `update --id 1 --by 2`, +**_not_** `update --id 1 --by +2`. See 'Example of usage 1' below. + +Example of usage 1: + +* `list` (_Optional_, done here to show change in habit count) +* `update --id 1 --by 2` + +Expected outcome 1: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Here is the current accumulation of your atomic habits! + Keep up the good work and you will develop a helpful habit in no time + 1. Make bed every morning [5] + 2. Read for at least 30 minutes every day [3] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + The following habit has been incremented! Keep up the good work! + 1. Make bed every morning [7] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +Example of usage 2: + +* `list` (_Optional_, done here to show change in habit count) +* `update --id 1 --by -2` + +Expected outcome 2: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Here is the current accumulation of your atomic habits! + Keep up the good work and you will develop a helpful habit in no time + 1.Make bed every morning [7] + 2.Read for at least 30 minutes every day [3] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + The following habit has been decremented. + 1.Make bed every morning [5] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + + + +### `delete` - Delete an atomic habit + +Delete an atomic habit that is not relevant anymore. + +Format: `delete --id HABIT_INDEX` + +* (**Optional**) Step 1: List the current habits using command `list` +* Step 2: Select the habit to delete by entering the index number of the habit, `HABIT_INDEX`, according to index of the + output from `list` + +Example of usage: + +* `list` (_Optional_) +* `delete --id 1` + +Expected outcome: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Here is the current accumulation of your atomic habits! + Keep up the good work and you will develop a helpful habit in no time + 1. Make bed every morning [5] + 2. Read for at least 30 minutes every day [3] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + The following habit has been deleted: + Make bed every morning [5] has been successfully deleted +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + + + +### `help` - Viewing Atomic Habit Help + +Lists all commands available and provide a short description of Atomic Habit feature. + +Format: `help [COMMAND_TO_CHECK]` + +* List all commands available in the Atomic Habit and a short description of the Atomic Habit +* Give a detailed explanation of the parameters and subcommands for the given command `COMMAND_TO_CHECK` (if specified) + +Example of usage 1: + +`help` + +Expected outcome: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + hb - Atomic Habits - Track and manage your habits with our suite of tools to help you grow and nurture a better you! + Input `help` to see all available commands. + Input `help [command-to-check] to get usage help for a specific command. + Here are all the commands available for you! + + 1. add - Add a habit to your habit tracker. + 2. delete - Delete the habit you don't want to continue. + 3. help - Get help on what commands can be used in Atomic Habit WellNUS++ + 4. home - Return back to the main menu of WellNUS++. + 5. list - Lists out all the habits in your tracker. + 6. update - Update how many times you've done a habit. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +Example of usage 2: + +`help add` + +Expected outcome: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + add - Add a habit to your habit tracker. + usage: add --name (your habit name) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + + + +## `gamif` - Accessing gamification feature + +Format: `gamif` + +Gamification system integrated into WellNUS++ to incentivize users to improve +their wellness. + +Users can accumulate XP points from working on their atomic habits and +level up. For example, do your recorded atomic habits and update the habit count +with the `update` command of the atomic habits feature to gain XP points. + +Example of usage: + +`gamif` + +Expected outcome: + +``` +###################################################################### + Welcome to + ______ _ _____ __ _ + / ____/___ _____ ___ (_) __(_)________ _/ /_(_)___ ____ + / / __/ __ `/ __ `__ \/ / /_/ / ___/ __ `/ __/ / __ \/ __ \ + / /_/ / /_/ / / / / / / / __/ / /__/ /_/ / /_/ / /_/ / / / / + \____/\__,_/_/ /_/ /_/_/_/ /_/\___/\__,_/\__/_/\____/_/ /_/ +###################################################################### +``` + +### `stats` - Gamification statistics + +Displays the user's current XP level and the number of XP points to reach the next level. + +Format: `stats` + +Example of usage: + +`stats` + +Expected outcome: + +``` +###################################################################### +# Current XP: Level 2 [===> ] # +# 7 more XP to Level 3 # +###################################################################### +``` + + + +### `help` - Viewing Gamification Help + +Lists all commands available and provide a short description of Gamification feature. + +Format: `help [COMMAND_TO_CHECK]` + +* List all commands available in the Gamification and a short description of the Gamification +* Give a detailed explanation of the parameters and subcommands for the given command `COMMAND_TO_CHECK` + +Example of usage 1: + +`help` + +Expected outcome: + +``` +###################################################################### + gamif - Gamification - Gamification gives you the motivation to continue improving your wellness by rewarding you for your efforts! + Input `help` to see all available commands. + Input `help [command-to-check]` to get usage help for a specific command. + Here are all the commands available for you! + + 1. help - Get help on what commands can be used in WellNUS++ Gamification Feature + 2. home - Returns the user to the main WellNus++ session + 3. stats - Displays the user's XP level and points +###################################################################### +``` + +Example of usage 2: + +`help stats` + +Expected outcome: + +``` +###################################################################### + stats - Displays the user's XP level and points + usage: stats +###################################################################### +``` + + + +## `reflect` - Accessing self reflection feature + +Format: `reflect` + +Self reflection feature allows users to get sets of random introspective questions to reflect on to improve overall +wellness and achieve better selves. + +Example of usage: + +`reflect` + +Expected outcome: + +``` +============================================================ + ##### ###### + # # ###### # ###### # # ###### ###### # ###### #### ##### + # # # # # # # # # # # # # + ##### ##### # ##### ###### ##### ##### # ##### # # + # # # # # # # # # # # # + # # # # # # # # # # # # # # + ##### ###### ###### # # # ###### # ###### ###### #### # +============================================================ + Welcome to WellNUS++ Self Reflection section :D + Feel very occupied and cannot find time to self reflect? + No worries, this section will give you the opportunity to reflect and improve on yourself!! +============================================================ +``` + + + +### `get` - Get reflection questions + +Ask WellNUS++ to generate a set of 5 random introspective questions for users to view and reflect on. +The questions are designed to be **randomised** for users to reflect on different aspects of life. + +Format: `get` + +Example of usage: + +`get` + +Expected outcome: + +``` +============================================================ + 1.What are three of my most cherished personal values? + 2.What is my purpose in life? + 3.What scares me the most right now? + 4.What is something that brings me joy? + 5.When is the last time I gave back to others? +============================================================ +``` + +* Since questions are randomised, the questions users receive might differ from the example outcome above. + + + +### `like` - Add reflection question into favorite list + +Users can add the reflection question they like into favorite list and review afterwards. + +Format: `like INDEX` + +* Adds the particular question generated in the previous set with index (`INDEX`) into the user's favorite list + +Note that the users are supposed to at least `get` a set of questions or use `prev` command to +review the previous set before liking them, and use the displayed index to choose questions. +Index parameter is limited to integer 1-5 as only 5 questions will be generated in every random set. + +Example of usage: + +`like 1` + +Expected output: + +``` +============================================================ + You have added question: "What is my purpose in life?" Into favorite list!! +============================================================ +``` + +* Since questions are randomised, a particular question might have different indexes in different random sets. + +### `fav` - View favorite list + +Users can review the list of reflection questions they liked. + +Format: `fav` + +Example of usage: + +`fav` + +Example output: + +``` +============================================================ + 1.What are three of my most cherished personal values? + 2.What is my purpose in life? + 3.Am I making time for my social life? +============================================================ +``` + +### `unlike` - Remove questions from favorite list + +Users can remove reflection questions from the favorite list. + +Format: `unlike INDEX` + +* Removes the particular question with index `INDEX` from the user's favorite list + +Take note that it is **recommended** (but _optional_) to use `fav` command to check the list of questions in the +favorite list before +unliking any of them, so that users are aware of which question they are removing. + +Example of usage step 1: + +`fav` (_Optional_) + +Example output step 1: + +``` +============================================================ + 1.What are three of my most cherished personal values? + 2.What is my purpose in life? + 3.Am I making time for my social life? +============================================================ +``` + +Example of usage step 2: + +`unlike 1` + +Example output: + +``` +============================================================ + You have removed question: "What are three of my most cherished personal values?" From favorite list!! +============================================================ +``` + +### `prev` - Get the previous set of reflection questions generated + +Users can view the previous set of questions generated for review. + +Format: `prev` + +Note that the users are supposed to at least `get` a set of questions before viewing the previous set. + +Example of usage: + +`prev` + +Example output: + +``` +============================================================ + 1.What is my purpose in life? + 2.What is my personality type? + 3.Did I make time for myself this week? + 4.Am I making time for my social life? + 5.What is something I find inspiring? +============================================================ +``` + + + +### `help` - Viewing Reflection Help + +Lists all commands available and provide a short description of Reflection feature. + +Format: `help [COMMAND_TO_CHECK]` + +* List all commands available in the Reflection and a short description of the Reflection +* Give a detailed explanation of the parameters and subcommands for the given command `COMMAND_TO_CHECK` + +Example of usage 1: + +`help` + +Expected outcome: + +``` +============================================================ + reflect - Self Reflection - Take some time to pause and reflect with our specially curated list of questions and reflection management tools. + Input `help` to see all available commands. + Input `help [command-to-check] to get usage help for a specific command. + Here are all the commands available for you! + + 1. fav - Get the list of questions that have been added to the favorite list. + 2. get - Get a list of questions to reflect on. + 3. help - Get help on what commands can be used in Reflection WellNUS++ + 4. home - Return back to the main menu of WellNUS++. + 5. like - Add a particular question to favorite list. + 6. unlike - Remove a particular question from favorite list. + 7. prev - Get the previously generated set of questions. +============================================================ +``` + +Example of usage 2: + +`help get` + +Expected outcome: + +``` +============================================================ + get - Get a list of questions to reflect on. + usage: get +============================================================ +``` + + + +## `ft` - Accessing Focus Timer Feature + +Our Focus Timer feature allows users to be productive by setting a configurable work-break timer, inspired by +the Pomodoro technique. When each timer ends, a `beep` is played to alert the user that the current timer countdown is +over. + +Summary of Pomodoro Technique: Repeated sessions of work followed by break to ensure maximum productivity during the +work cycle and allow one to relax sufficiently during the break cycle before working again. Longer breaks are taken +after a few consecutive sessions(**2** by default in our app). Find out more about the technique on the +[Wikipedia page](https://en.wikipedia.org/wiki/Pomodoro_Technique). + +**Command input is disabled when timer is counting down the last 10 seconds.** + +Format: `ft`
+ +Example of usage:
+ +`ft` + +Expected outcome: + +``` +************************************************************ + Welcome to Focus Timer. + Start a focus session with `start`, or `config` the session first! +************************************************************ +``` + + + +### `start` - Start Session + +Ask WellNUS++ to start the focus session consisting of work and break cycles. + +`start` can only be used when you first enter Focus, after a session has ended or after a session has been `stop`. + +Format: `start` + +Example of usage: + +`start` + +Expected outcome: + +``` +************************************************************ + Your session has started. All the best! +************************************************************ +************************************************************ + Task Cycle: Do your task now! +************************************************************ +``` + +### `pause` - Pause session + +Ask WellNUS++ to pause the focus session which pauses the current countdown timer. + +`pause` can only be used when the timer is counting down. + +Format: `pause` + +Example of usage: + +`pause` + +Expected outcome: + +``` +************************************************************ + Timer paused at: 0:54 +************************************************************ +``` + +### `resume` - Resume session + +Ask WellNUS++ to resume the focus session which continues the current countdown timer. + +`resume` can only be used when the timer has been paused. + +Format: `resume` + +Example of usage: + +`resume` + +Expected outcome: + +``` +************************************************************ + Timer resumed at: 0:54 +************************************************************ +``` + +### `check` - Check time + +Ask WellNUS++ to display the current time of the timer for users to check time remaining. + +`check` can be used whenever during the ongoing session. If the timer has not `start`, is `stop` or reached 0 and is +waiting for the `next` command, check will not run. + +Format: `check` + +Example of usage: + +`check` + +Expected outcome: + +``` +************************************************************ + Time left: 0:57 +************************************************************ +``` + +### `next` - Next timer + +Ask WellNUS++ to start the next work or break iteration of the focus session. + +`next` can only be used when a work or break timer has ended, and a prompt to proceed to the next timer is displayed. + +Format: `next` + +Example of usage: + +`next` + +Expected outcome (if the next timer is a work timer): + +``` +************************************************************ + Task Cycle: Do your task now! +************************************************************ +``` + +Expected outcome (if the next timer is a break timer): + +``` +************************************************************ + Break Cycle: Take a breather! +************************************************************ +``` + +### `stop` - Stop session + +Ask WellNUS++ to stop the focus session. + +`stop` can only be used after the session has started. + +Format: `stop` + +Example of usage: + +`stop` + +Expected outcome: + +``` +************************************************************ + Your focus session has ended. + To start a new session, `start` it up! + You can also configure the session to your liking with `config`! +************************************************************ +``` + + + +### `help` - Viewing Focus Timer help + +Lists all commands available and provide a short description of Focus Timer feature. + +Format: `help [COMMAND_TO_CHECK]` + +* List all commands available in the Focus Timer and a short description of the Focus Timer +* Give a detailed explanation of the parameters and subcommands for the given command `COMMAND_TO_CHECK` + +Example of usage 1: + +`help` + +Expected outcome: + +``` +************************************************************ + ft - Focus Timer - Set a configurable 'Pomodoro' timer with work and rest cycles to keep yourself focused and productive! + Input `help` to see all available commands. + Input `help [command-to-check]` to get usage help for a specific command. + Here are all the commands available for you! + + 1. check - Check the time left in the current countdown.Only usable when a countdown is not finished! + 2. config - Change the number of cycles and length of your work, break and longbreak timings! + 3. help - Get help on what commands can be used in Focus Timer WellNUS++ + 4. home - Stop the session and go back to WellNUS++. + 5. next - Move on to the next countdown. Can only be used when a countdown timer has ended. + 6. pause - Pause the session! Can only be used when a countdown is ticking. + 7. resume - Continue the countdown. Can only be used when a countdown is paused. + 8. start - Start your focus session! + 9. stop - Stop the session. You will have to `start` your focus session again! +************************************************************ + +``` + +Example of usage 2: + +`help stop` + +Expected outcome: + +``` +************************************************************ + stop - Stop the session. You will have to `start` your focus session again! + usage: stop +************************************************************ +``` + + + +### `config` - Configure the Timer + +Configures the focus timer's settings. +The number of work-break cycles, work length and break length can be configured. +When leaving `ft`, the configuration will be reset to the default values. + +Format: `config [--cycle NUM_OF_CYCLE --work WORK_TIME --break BREAK_TIME --longbreak LONG_BREAK_TIME]` + +* If no arguments are given, `config` prints out the current session settings +* `NUM_OF_CYCLE` is an **integer** that is `>= 2` +* `WORK_TIME, BREAK_TIME, LONG_BREAK_TIME` are **integers** that are all `>= 1` + +**Configuation Limits** + +* `LONG_BREAK_TIME` should be greater or equal to `BREAK_TIME` +* `WORK_TIME, BREAK_TIME, LONG_BREAK_TIME` have an upper limit of 60 +* `NUM_CYCLE` has an upper limit of 5 + +Why limit to 60 mins and 5 cycles? +[Studies have shown](https://www.lib.sfu.ca/about/branches-depts/slc/learning/exam-prep/efficient-effective-study) +that an hour of studying/task at a time is the most optimal. 5 cycles has been set to prevent guard you against +excessive working. Anything higher than the upper limits may be counterproductive! + +**Default values for Focus Timer** + +* `NUM_OF_CYCLE = 2` +* `WORK_TIME = 1` +* `BREAK_TIME = 1` +* `LONG_BREAK_TIME = 1` + +Example of usage: + +`config` + +Expected outcome: + +``` +************************************************************ + Okay, here's your configured session details! + Cycles: 2 + Work: 1 minute + Break: 1 minute + Long break: 1 minute +************************************************************ +``` + +Example of usage 2: + +`config --longbreak 10 --cycle 4 --work 30 --break 5` + +Expected outcome: + +``` +************************************************************ + Okay, here's your configured session details! + Cycles: 4 + Work: 30 minutes + Break: 5 minutes + Long break: 10 minutes +************************************************************ +``` + + + +## `home` - Return back main WellNUS++ + +To leave the current feature and return back to main interface. Each individual feature (i.e. atomic habit, +self reflection, focus timer and gamification) has this command with customised +output messages. + +Format: `home` + +Example of usage 1: + +`home` + +Expected outcome for atomic habit: + +``` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Thank you for using atomic habits. Do not forget about me! +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +Example of usage 2: + +`home` + +Expected outcome for self reflection: + +``` +============================================================ + How do you feel after reflecting on yourself? + Hope you have gotten some takeaways from self reflection, see you again!! +============================================================ +``` + +**Note**: These are just particular examples taken from specific features, and are **_not representative_** of +other features' `home` commands. Please try the `home` command inside our application to view the actual customised +output for other features. + +## `exit` - Exit WellNUS++ + +Exits `WellNUS++`. Data of the current progress will be saved in data files and restored at the next launch +of `WellNUS++`. + +Format: `exit` + +Take note that users are only allowed to exit from **main** WellNUS++ (i.e. users cannot exit the program from +individual features like atomic habit, and the `exit` command is not recognised inside feature sessions). + +Example of usage: + +`exit` + +Expected outcome: + +``` +------------------------------------------------------------ + Thank you for using WellNUS++! See you again soon Dx +------------------------------------------------------------ +``` + + ## FAQ -**Q**: How do I transfer my data to another computer? +**Q**: What do I need to run `WellNUS++` on my computer? -**A**: {your answer here} +**A**: Your computer needs to have **Java 11 or above** installed. The operating system(Windows, macOS, etc) doesn't +matter. -## Command Summary +**Q**: Would my data be saved after I close the `WellNUS++`? + +**A**: Yes. All ours features will store data inside a `data` folder relative to where you placed the `WellNUS++` jar. +The next time you run `WellNUS++`, all your data will be restored. + +**Q**: Do I need to be connected to the Internet to run `WellNUS++`? + +**A**: No. `WellNUS++` runs offline to help you focus better. Your data is also saved locally, so it is preserved even +without an Internet connection. + +**Q**: Can I transfer my data to another computer? -{Give a 'cheat sheet' of commands here} +**A**: Yes. Copy the `data` folder found in the same path as the `WellNUS++` jar file to another +computer. All your data will be restored the next time you run `WellNUS++`. + +**Q**: How can I navigate through the program? + +**A**: In `WellNUS++`, type `help` to view the list of feature supported by our app. From within a feature, type `help` +to learn about the commands available within that feature. + + + +## Command Summary -* Add todo `todo n/TODO_NAME d/DEADLINE` +* Help `help [COMMAND_TO_CHECK]` +* Access feature`hb` + * Add habit `add --name ATOMIC_HABIT_NAME` + * View habit `list` + * Update habit `update --id HABIT_INDEX [--by NUMBER_TO_CHANGE]` + * Delete habit `delete --id HABIT_INDEX` +* Access feature `reflect` + * Get reflect question `get` + * Like reflect question `like INDEX` + * View favorite list `fav` + * Unlike reflect question `unlike INDEX` + * View previous questions `prev` +* Access feature `gamif` + * Display gamification statistics `stats` +* Access feature `ft` + * Start the timer `start` + * Pause the timer `pause` + * Resume the timer `resume` + * Check the time left `check` + * Go to the next countdown `next` + * Stop the timer `stop` + * Configure the + timer `config [--cycle NUM_OF_CYCLE --work WORK_TIME --break BREAK_TIME --longbreak LONG_BREAK_TIME]` +* Return to main interface `home` +* Exit program `exit` diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000000..872efe9b14 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,5 @@ +remote_theme: pages-themes/modernist@v0.2.0 +plugins: +- jekyll-remote-theme # add this line to the plugins list if you already have one +title: WellNUS++ +description: An app that help NUS Computing students improve their wellness:D diff --git a/docs/diagrams/AtomicHabit.png b/docs/diagrams/AtomicHabit.png new file mode 100644 index 0000000000..2b020d8223 Binary files /dev/null and b/docs/diagrams/AtomicHabit.png differ diff --git a/docs/diagrams/AtomicHabit.puml b/docs/diagrams/AtomicHabit.puml new file mode 100644 index 0000000000..1ceb0a10b1 --- /dev/null +++ b/docs/diagrams/AtomicHabit.puml @@ -0,0 +1,52 @@ +@startuml +'https://plantuml.com/class-diagram + +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + + +Class MainManager + +Class AtomicHabitManager { ++ runEventDriver() +} + +Class AtomicHabitList { ++ addAtomicHabit(AtomicHabit) ++ deleteAtomicHabit(AtomicHabit) ++ getAllHabits(): ArrayList ++ getHabitByIndex(Integer): AtomicHabit +} + +Class AtomicHabit { +- description: String +- count: int ++ increaseCount() ++ decreaseCount() ++ getDescription(): String ++ getCount(): int ++ toString(): String +} + +Class AtomicHabitCommand { ++ execute() +} + +Class "{abstract}\nCommand" as Command + +MainManager --> AtomicHabitManager : > Executes + +AtomicHabitManager --> "1 " AtomicHabitList : > Initializes + +AtomicHabitManager -down-> "6" AtomicHabitCommand : > Calls + +AtomicHabitCommand -right-> "1" AtomicHabitList: > Controls + +AtomicHabitCommand -up-|> Command + +AtomicHabitList -right-> "0..*" AtomicHabit : > Manages + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/AtomicHabitSequenceDiagram.png b/docs/diagrams/AtomicHabitSequenceDiagram.png new file mode 100644 index 0000000000..a5db4d6dec Binary files /dev/null and b/docs/diagrams/AtomicHabitSequenceDiagram.png differ diff --git a/docs/diagrams/AtomicHabitSequenceDiagram.puml b/docs/diagrams/AtomicHabitSequenceDiagram.puml new file mode 100644 index 0000000000..353871ae7c --- /dev/null +++ b/docs/diagrams/AtomicHabitSequenceDiagram.puml @@ -0,0 +1,124 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR +hide footbox + +participant ":MainManager" as MainManager MODEL_COLOR +participant ":AtomicHabitManager" as AtomicHabitManager MODEL_COLOR +participant ":AtomicUi" as AtomicUi MODEL_COLOR +participant ":HabitList" as HabitList MODEL_COLOR +participant ":AddCommand" as AddCommand MODEL_COLOR +participant ":DeleteCommand" as DeleteCommand MODEL_COLOR +participant ":ListCommand" as ListCommand MODEL_COLOR +participant ":UpdateCommand" as UpdateCommand MODEL_COLOR +participant ":AtomicHabitCommand" as AtomicHabitCommand MODEL_COLOR +participant ":HelpCommand" as HelpCommand MODEL_COLOR +participant ":HomeCommand" as HomeCommand MODEL_COLOR +participant ":BadCommandException" as BadCommandException MODEL_COLOR + +activate MainManager + +create AtomicHabitManager +MainManager -> AtomicHabitManager +activate AtomicHabitManager + +create AtomicUi +AtomicHabitManager -> AtomicUi +activate AtomicUi +AtomicUi --> AtomicHabitManager +deactivate AtomicUi + +create HabitList +AtomicHabitManager -> HabitList +activate HabitList +HabitList --> AtomicHabitManager +deactivate HabitList + +AtomicHabitManager --> MainManager +deactivate AtomicHabitManager + +MainManager -> AtomicHabitManager : runEventDriver() +activate AtomicHabitManager + +AtomicHabitManager -> AtomicHabitManager : greet() +activate AtomicHabitManager +AtomicHabitManager -> AtomicUi : getTextUi() +activate AtomicUi +AtomicUi --> AtomicHabitManager : AtomicUi +deactivate AtomicUi +AtomicHabitManager -> AtomicUi : printOutputMessage() +deactivate AtomicHabitManager + +AtomicHabitManager -> AtomicHabitManager : runCommands() +activate AtomicHabitManager + + loop until isExit is true + AtomicHabitManager -> AtomicHabitManager : getCommand() + activate AtomicHabitManager + AtomicHabitManager -> AtomicUi : getTextUi() + activate AtomicUi + AtomicUi --> AtomicHabitManager : AtomicUi + deactivate AtomicUi + AtomicHabitManager -> AtomicUi : getCommand() + activate AtomicUi + AtomicUi --> AtomicHabitManager : commandString + deactivate AtomicUi + deactivate AtomicHabitManager + + AtomicHabitManager -> AtomicHabitManager : getCommandFor(commandString) + activate AtomicHabitManager + alt commandString = add + create AddCommand + AtomicHabitManager -> AddCommand + activate AddCommand + AddCommand --> AtomicHabitManager : AddCommand(arguments, HabitList) + deactivate AddCommand + else commandString = delete + create DeleteCommand + AtomicHabitManager -> DeleteCommand + activate DeleteCommand + DeleteCommand --> AtomicHabitManager : DeleteCommand(arguments, HabitList) + deactivate DeleteCommand + else commandString = home + create HomeCommand + AtomicHabitManager -> HomeCommand + activate HomeCommand + HomeCommand --> AtomicHabitManager : HomeCommand(arguments, HabitList) + deactivate HomeCommand + else commandString = list + create ListCommand + AtomicHabitManager -> ListCommand + activate ListCommand + ListCommand --> AtomicHabitManager : ListCommand(arguments, HabitList) + deactivate ListCommand + else commandString = update + create UpdateCommand + AtomicHabitManager -> UpdateCommand + activate UpdateCommand + UpdateCommand --> AtomicHabitManager : UpdateCommand(arguments, HabitList) + deactivate UpdateCommand + else commandString = help + create HelpCommand + AtomicHabitManager -> HelpCommand + activate HelpCommand + HelpCommand --> AtomicHabitManager : HelpCommand(arguments, HabitList) + deactivate HelpCommand + else + create BadCommandException + AtomicHabitManager -> BadCommandException + activate BadCommandException + BadCommandException --> AtomicHabitManager + deactivate BadCommandException + end + deactivate AtomicHabitManager + AtomicHabitManager -> AtomicHabitCommand : execute() + end +deactivate AtomicHabitManager + +deactivate AtomicHabitManager +AtomicHabitManager --> MainManager + +@enduml diff --git a/docs/diagrams/CommandParserClass.png b/docs/diagrams/CommandParserClass.png new file mode 100644 index 0000000000..dbf11bf0af Binary files /dev/null and b/docs/diagrams/CommandParserClass.png differ diff --git a/docs/diagrams/CommandParserClass.puml b/docs/diagrams/CommandParserClass.puml new file mode 100644 index 0000000000..2e5285dea3 --- /dev/null +++ b/docs/diagrams/CommandParserClass.puml @@ -0,0 +1,50 @@ +@startuml +'https://plantuml.com/class-diagram +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + + +Class CommandParser { + + parseUserInput(String) + + getMainArgument(String) + - splitIntoCommands(String) + - getArgumentFromCommand(String) + - getPayloadFromCommand(String) +} + + +Class "{abstract}\nManager" as Manager { + # commandParser : CommandParser + + getCommandParser() +} + +Class AtomicHabitManager { + # commandParser : CommandParser + + getCommandParser() +} + +Class ReflectionManager { + # commandParser : CommandParser + + getCommandParser() +} + +Class FocusManager { + # commandParser : CommandParser + + getCommandParser() +} + +Class GamificationManager { + # commandParser : CommandParser + + getCommandParser() +} + + +CommandParser "1" -- Manager : contains < +Manager <|-- AtomicHabitManager +Manager <|-- ReflectionManager +Manager <|-- GamificationManager +Manager <|-- FocusManager + +@enduml \ No newline at end of file diff --git a/docs/diagrams/CommandParserSequence.png b/docs/diagrams/CommandParserSequence.png new file mode 100644 index 0000000000..ee978bb29b Binary files /dev/null and b/docs/diagrams/CommandParserSequence.png differ diff --git a/docs/diagrams/CommandParserSequence.puml b/docs/diagrams/CommandParserSequence.puml new file mode 100644 index 0000000000..fca947e053 --- /dev/null +++ b/docs/diagrams/CommandParserSequence.puml @@ -0,0 +1,86 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR +hide footbox + + +participant ":FooUi" as Ui MODEL_COLOR +participant ":FooManager" as Manager MODEL_COLOR +participant ":CommandParser" as CommandParser MODEL_COLOR +participant ":FooCommand" as FooCommand MODEL_COLOR + +activate Manager + + +Manager -> Ui: getCommand() + +note right of Manager +For brevity, we ignore the +inner workings of the +subclass of TextUi (FooUi) +on how it obtains user input +end note + +activate Ui +Ui --> Manager: userInput +deactivate Ui + + +Manager -> CommandParser : parseUserInput(userInput) +activate CommandParser +CommandParser -> CommandParser : splitIntoCommands(userInput) +activate CommandParser +CommandParser --> CommandParser : String[] commands +deactivate CommandParser + loop for each command in commands + CommandParser -> CommandParser :getArgumentFromCommand(command) + activate CommandParser + CommandParser --> CommandParser : argument + deactivate CommandParser + CommandParser -> CommandParser :getPayloadFromCommand(command) + activate CommandParser + CommandParser --> CommandParser : payload + deactivate CommandParser + end +CommandParser --> Manager: HashMap argumentPayload +deactivate CommandParser + +Manager -> CommandParser: getMainArgument(userInput) +activate CommandParser +CommandParser --> Manager :mainArgument +deactivate CommandParser + +note left of Manager +The following alt frame +represents a long switch-case +which matches mainArgument +to the correct command to be +constructed and executed. + +For readability, only two +conditions are shown. +1. The case where it matches, +2. The case where it doesn't. +end note + +alt mainArgument = FOO_COMMAND_KEYWORD + create FooCommand + Manager -> FooCommand : FooCommand(argumentPayload) + activate FooCommand + FooCommand --> Manager + deactivate FooCommand + Manager -> FooCommand: execute() + activate FooCommand + FooCommand --> Manager + deactivate FooCommand +else default + Manager -> Manager: printErrorFor() + activate Manager + Manager --> Manager + deactivate Manager + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/FocusTimerClassDiagram.png b/docs/diagrams/FocusTimerClassDiagram.png new file mode 100644 index 0000000000..7979008337 Binary files /dev/null and b/docs/diagrams/FocusTimerClassDiagram.png differ diff --git a/docs/diagrams/FocusTimerClassDiagram.puml b/docs/diagrams/FocusTimerClassDiagram.puml new file mode 100644 index 0000000000..b56ecf9c3b --- /dev/null +++ b/docs/diagrams/FocusTimerClassDiagram.puml @@ -0,0 +1,68 @@ +@startuml +'https://plantuml.com/class-diagram + +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + + +Class MainManager + +Class FocusManager { ++ runEventDriver() +} + +Class Session { +- currentCountdownIndex: int +- cycle: int +- work: int +- brk: int +- longBrk: int ++ startTimer() ++ getCurrentCountdown(): Countdown ++ isSessionReady(): boolean ++ isSessionCounting(): boolean ++ isSessionWaiting(): boolean ++ isSessionPaused(): boolean +} + +Class Countdown { +- timer: Timer +- minutes: int +- seconds: int +- isCompletedCountdown: AtomicBoolean +- isRunClock: AtomicBoolean +- isReady: boolean ++ start() ++ setStop() ++ setStart() ++ setPause() ++ getMinutes(): int ++ getSeconds(): int ++ getIsRunning(): boolean ++ getIsCompletedCountdown(): boolean ++ isCountdownPrinting(): boolean ++ getIsReady(): boolean +} + +Class FocusCommand { ++ execute() +} + +Class "{abstract}\nCommand" as Command + +MainManager --> FocusManager : > Executes + +FocusManager --> "1 " Session : > Initializes + +FocusManager -down-> "9" FocusCommand : > Calls + +FocusCommand -right-> "1" Session: > Controls + +FocusCommand -up-|> Command + +Session -right-> "4..*" Countdown : > Manages + + +@enduml diff --git a/docs/diagrams/FocusTimerState.png b/docs/diagrams/FocusTimerState.png new file mode 100644 index 0000000000..b27e4610b7 Binary files /dev/null and b/docs/diagrams/FocusTimerState.png differ diff --git a/docs/diagrams/FocusTimerState.puml b/docs/diagrams/FocusTimerState.puml new file mode 100644 index 0000000000..07051713d0 --- /dev/null +++ b/docs/diagrams/FocusTimerState.puml @@ -0,0 +1,20 @@ +@startuml +'https://plantuml.com/state-diagram + +hide empty description + +[*] --> Ready + +Ready --> Ready: config +Ready --> Counting : start +Counting -up-> Ready : stop +Counting -down-> Waiting +Counting --> Counting: check +Waiting --> Counting : next +Waiting --> Ready: stop +Paused -down-> Ready: stop +Paused -up-> Counting: resume +Paused -up-> Paused: check +Counting --> Paused: pause + +@enduml diff --git a/docs/diagrams/GamificationClassDiagram.png b/docs/diagrams/GamificationClassDiagram.png new file mode 100644 index 0000000000..47c456bcfa Binary files /dev/null and b/docs/diagrams/GamificationClassDiagram.png differ diff --git a/docs/diagrams/GamificationClassDiagram.puml b/docs/diagrams/GamificationClassDiagram.puml new file mode 100644 index 0000000000..3c51c8220f --- /dev/null +++ b/docs/diagrams/GamificationClassDiagram.puml @@ -0,0 +1,76 @@ +@startuml +'https://plantuml.com/class-diagram +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +class GamificationData { + - xp: int = 0 + - level: int + + addXp(pointsToAdd: int): boolean + + getXpForCurrentLevelOnly(): int + + getTotalXp(): int + + getXpLevel(): int + + getXpToReachNextLevel(): int + + minusXp(pointsToMinus: int): boolean +} +class GamificationStorage { + + cleanDataFile() + + loadData(): GamificationData + + store(data: GamificationData) +} +class Storage { + + checkFileExists(fileName: String): boolean + + loadData(fileName: String): ArrayList + + saveData(tokenizedManager: ArrayList, fileName: String) +} +GamificationData <..> GamificationStorage +GamificationStorage --> "1" Storage + +class "<>\nTokenizer" as Tokenizer { +} +class GamificationTokenizer { + + tokenize(dataObjects: ArrayList): ArrayList + + detokenize(tokenizedDataObjects: ArrayList): ArrayList +} +Tokenizer <|.. GamificationTokenizer +GamificationStorage -> "1" GamificationTokenizer +GamificationData <.. GamificationTokenizer + +class "{abstract}\nCommand" as Command +class HelpCommand { +} +class HomeCommand { +} +class StatsCommand { +} +Command <|-- HelpCommand +Command <|-- HomeCommand +Command <|-- StatsCommand +StatsCommand ---> "1" GamificationData + +class TextUi { +} +class GamificationUi { + + {static} printCelebrateLevelUp() + + {static} printGoodbye() + + {static} printLogo() + + {static} printGamificationMessage(msg: String) + + {static} printXpBar(gamData: GamificationData, shouldPrintXpRemaining: boolean) +} +TextUi <|-- GamificationUi +GamificationData <. GamificationUi + +class "{abstract}\nManager" as Manager +class GamificationManager { + + getGamificationData(): GamificationData +} +Manager <|-- GamificationManager +GamificationManager --> "1" GamificationData +GamificationManager --> "1" GamificationUi +GamificationManager ..> HelpCommand +GamificationManager ..> HomeCommand +GamificationManager ..> StatsCommand + +@enduml diff --git a/docs/diagrams/Manager.png b/docs/diagrams/Manager.png new file mode 100644 index 0000000000..e663ef6f05 Binary files /dev/null and b/docs/diagrams/Manager.png differ diff --git a/docs/diagrams/Manager.puml b/docs/diagrams/Manager.puml new file mode 100644 index 0000000000..2fe3ad813c --- /dev/null +++ b/docs/diagrams/Manager.puml @@ -0,0 +1,46 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Package common { +Class MainManager { ++greet() ++runEventDriver() +} +} + +Class WellNus { ++greet() ++executeUserCommands() ++byeUser() +} + +Package manager { +Class "{abstract}\nManager" as Manager { ++{abstract} runEventDriver() +} +} + +Package atomichabit { +Class AtomicHabitManager { ++runEventDriver() +} +} + +Package reflection { +Class ReflectionManager { ++runEventDriver() +} +} + +Manager <|-- MainManager +Manager <|- AtomicHabitManager +Manager <|- ReflectionManager + +WellNus -> "1" MainManager : executes > +MainManager --> "1" AtomicHabitManager : calls > +MainManager --> "1" ReflectionManager : calls > + +@enduml diff --git a/docs/diagrams/ReflectionClassDiagram.png b/docs/diagrams/ReflectionClassDiagram.png new file mode 100644 index 0000000000..bac603f40f Binary files /dev/null and b/docs/diagrams/ReflectionClassDiagram.png differ diff --git a/docs/diagrams/ReflectionClassDiagram.puml b/docs/diagrams/ReflectionClassDiagram.puml new file mode 100644 index 0000000000..928246c321 --- /dev/null +++ b/docs/diagrams/ReflectionClassDiagram.puml @@ -0,0 +1,53 @@ +@startuml +!include style.puml +skinparam classAttributeIconSize 0 +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +class ReflectionManager{ +-reflectUi : ReflectUi ++executeCommands() : void ++runEventDriver() : void +} +class ReflectUi{ +#printLogoWithSeparator(logo : String) : void +} +class ReflectionQuestion{ +-questionDescription : String +} +class QuestionList{ +-setUpQuestions() : void ++getQuestions() : ArrayList +} + +class ReflectionCommands + +class TextUi{ ++printOutputMessage(message : String) : void ++printErrorFor(exception : Exception, additionalMessage : String) : void +} +class "{abstract}\nManager" as Manager{ +#{abstract}setSupportedCommands() : void +#{abstract}runEventDriver() : void +} +class "{abstract}\nCommand" as Command{ +-arguments : HashMap +#{abstract}execute : void +#{abstract}validateCommand(commandMap: HashMap) : void +} +class Storage +class ReflectionTokenizer + +ReflectionManager --> "1" QuestionList : create > +ReflectionManager --|> Manager +ReflectionManager ..> ReflectionCommands +ReflectionCommands --> "1" ReflectUi : uses > +ReflectionCommands ..> QuestionList +QuestionList --> "10" ReflectionQuestion : contains > +QuestionList --> "1" ReflectUi : uses > +QuestionList --> "1" Storage : uses > +QuestionList --> "1" ReflectionTokenizer : uses > +ReflectUi --|> TextUi +ReflectionCommands --|> Command +@enduml diff --git a/docs/diagrams/ReflectionCommandsUML.png b/docs/diagrams/ReflectionCommandsUML.png new file mode 100644 index 0000000000..3cc49db4d9 Binary files /dev/null and b/docs/diagrams/ReflectionCommandsUML.png differ diff --git a/docs/diagrams/ReflectionCommandsUML.puml b/docs/diagrams/ReflectionCommandsUML.puml new file mode 100644 index 0000000000..64d8d2cd40 --- /dev/null +++ b/docs/diagrams/ReflectionCommandsUML.puml @@ -0,0 +1,32 @@ +@startuml +!include style.puml +skinparam classAttributeIconSize 0 +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Package command <>{ +class GetCommand{ ++getRandomQuestions() : ArrayList +} +class FavCommand{ +} +class PrevCommand{ ++getPrevSetQuestions() : void +} +class LikeCommand{ ++addFavQuestion(questionIndex: String) ++mapInputToQuestion() : HashMap +} +class UnlikeCommand{ ++removeFavQuestion(questionIndex: String) : void +} + +class HelpCommand{ ++printHelpMessage() : void +} +class HomeCommand{ +} +} + +@enduml diff --git a/docs/diagrams/ReflectionSequenceDiagram.png b/docs/diagrams/ReflectionSequenceDiagram.png new file mode 100644 index 0000000000..884c9e7157 Binary files /dev/null and b/docs/diagrams/ReflectionSequenceDiagram.png differ diff --git a/docs/diagrams/ReflectionSequenceDiagram.puml b/docs/diagrams/ReflectionSequenceDiagram.puml new file mode 100644 index 0000000000..8c6c4f7fcb --- /dev/null +++ b/docs/diagrams/ReflectionSequenceDiagram.puml @@ -0,0 +1,184 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR +hide footbox + +participant ":MainManager" as MainManager MODEL_COLOR +participant ":ReflectionManager" as ReflectionManager MODEL_COLOR +participant ":ReflectUi" as ReflectUi MODEL_COLOR +participant ":CommandParser" as CommandParser MODEL_COLOR +participant ":QuestionList" as QuestionList MODEL_COLOR +participant ":GetCommand" as GetCommand MODEL_COLOR +participant ":LikeCommand" as LikeCommand MODEL_COLOR +participant ":UnlikeCommand" as UnlikeCommand MODEL_COLOR +participant ":PrevCommand" as PrevCommand MODEL_COLOR +participant ":FavoriteCommand" as FavoriteCommand MODEL_COLOR +participant ":HelpCommand" as HelpCommand MODEL_COLOR +participant ":HomeCommand" as HomeCommand MODEL_COLOR +participant ":BadCommandException" as BadCommandException MODEL_COLOR + +activate MainManager + +create ReflectionManager +MainManager -> ReflectionManager: +activate ReflectionManager + +create ReflectUi +ReflectionManager -> ReflectUi: +activate ReflectUi +ReflectUi --> ReflectionManager +deactivate ReflectUi + +create CommandParser +ReflectionManager -> CommandParser: +activate CommandParser +CommandParser --> ReflectionManager +deactivate CommandParser + +create QuestionList +ReflectionManager -> QuestionList: +activate QuestionList +QuestionList --> ReflectionManager +deactivate QuestionList + +ReflectionManager --> MainManager +deactivate ReflectionManager + +MainManager -> ReflectionManager : runEventDriver() +activate ReflectionManager + +loop until isExit is true + + ReflectionManager -> ReflectUi : getCommands() + activate ReflectUi + ReflectUi --> ReflectionManager : inputCommand: String + deactivate ReflectUi + + ReflectionManager -> CommandParser : setCommand(inputCommand: String) + activate CommandParser + CommandParser --> ReflectionManager + deactivate CommandParser + + ReflectionManager -> ReflectionManager : executeCommands() + activate ReflectionManager + alt commandType = get + create GetCommand + ReflectionManager -> GetCommand + activate GetCommand + GetCommand --> ReflectionManager + deactivate GetCommand + + ReflectionManager -> GetCommand : execute() + activate GetCommand + + GetCommand --> ReflectionManager + deactivate GetCommand + destroy GetCommand + + else commandType = like + create LikeCommand + ReflectionManager -> LikeCommand + activate LikeCommand + LikeCommand --> ReflectionManager + deactivate LikeCommand + + ReflectionManager -> LikeCommand : execute() + activate LikeCommand + + LikeCommand --> ReflectionManager + deactivate LikeCommand + destroy LikeCommand + + else commandType = unlike + create UnlikeCommand + ReflectionManager -> UnlikeCommand + activate UnlikeCommand + UnlikeCommand --> ReflectionManager + deactivate UnlikeCommand + + ReflectionManager -> UnlikeCommand : execute() + activate UnlikeCommand + + UnlikeCommand --> ReflectionManager + deactivate UnlikeCommand + destroy UnlikeCommand + + else commandType = prev + create PrevCommand + ReflectionManager -> PrevCommand + activate PrevCommand + PrevCommand --> ReflectionManager + deactivate PrevCommand + + ReflectionManager -> PrevCommand : execute() + activate PrevCommand + + PrevCommand --> ReflectionManager + deactivate PrevCommand + destroy PrevCommand + + else commandType = fav + create FavoriteCommand + ReflectionManager -> FavoriteCommand + activate FavoriteCommand + FavoriteCommand --> ReflectionManager + deactivate FavoriteCommand + + ReflectionManager -> FavoriteCommand : execute() + activate FavoriteCommand + + FavoriteCommand --> ReflectionManager + deactivate FavoriteCommand + destroy FavoriteCommand + + else commandType = help + create HelpCommand + ReflectionManager -> HelpCommand + activate HelpCommand + HelpCommand --> ReflectionManager + deactivate HelpCommand + + ReflectionManager -> HelpCommand : execute() + activate HelpCommand + + HelpCommand --> ReflectionManager + deactivate HelpCommand + destroy HelpCommand + + else commandType = home + create HomeCommand + ReflectionManager -> HomeCommand + activate HomeCommand + HomeCommand --> ReflectionManager + deactivate HomeCommand + + ReflectionManager -> HomeCommand : execute() + activate HomeCommand + + HomeCommand --> ReflectionManager + deactivate HomeCommand + destroy HomeCommand + + else + create BadCommandException + ReflectionManager -> BadCommandException + activate BadCommandException + BadCommandException --> ReflectionManager + deactivate BadCommandException + + ReflectionManager -> BadCommandException + activate BadCommandException + + BadCommandException --> ReflectionManager + deactivate BadCommandException + destroy BadCommandException + end + ReflectionManager --> ReflectionManager + deactivate ReflectionManager +end +ReflectionManager --> MainManager +deactivate ReflectionManager +@enduml diff --git a/docs/diagrams/StorageSequence-Saving_Data__Emphasis_on_Storage_Subroutine_.png b/docs/diagrams/StorageSequence-Saving_Data__Emphasis_on_Storage_Subroutine_.png new file mode 100644 index 0000000000..f58b68f3e8 Binary files /dev/null and b/docs/diagrams/StorageSequence-Saving_Data__Emphasis_on_Storage_Subroutine_.png differ diff --git a/docs/diagrams/StorageSequence.puml b/docs/diagrams/StorageSequence.puml new file mode 100644 index 0000000000..2d878f14b2 --- /dev/null +++ b/docs/diagrams/StorageSequence.puml @@ -0,0 +1,41 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Participant ":FooManager" as FooManager MODEL_COLOR +Participant ":FooTokenizer" as FooTokenizer MODEL_COLOR +Participant ":Storage" as Storage MODEL_COLOR + +hide footbox +title Saving Data (Emphasis on Storage Subroutine) +FooManager -> FooTokenizer : tokenize(ArrayList) +activate FooTokenizer +activate FooManager +FooTokenizer --> FooManager: ArrayList tokenizedManager +deactivate FooTokenizer +FooManager -> Storage : saveData(ArrayList tokenizedManager, fileName) +activate Storage +Storage -> Storage: isValidFileName(fileName) +activate Storage +Storage --> Storage +deactivate Storage +Storage -> Storage: getFile() +activate Storage +Storage --> Storage: file +deactivate Storage +Storage -> Storage: tokenizeString(tokenizedManager) +activate Storage +Storage --> Storage: tokenizedString +deactivate Storage +Storage -> Storage: writeDataToDisk(tokenizedString, file) +activate Storage +Storage --> Storage +deactivate Storage +Storage --> FooManager +deactivate Storage + +@enduml diff --git a/docs/diagrams/Tokenizer.png b/docs/diagrams/Tokenizer.png new file mode 100644 index 0000000000..7ce13619a1 Binary files /dev/null and b/docs/diagrams/Tokenizer.png differ diff --git a/docs/diagrams/Tokenizer.puml b/docs/diagrams/Tokenizer.puml new file mode 100644 index 0000000000..6360246066 --- /dev/null +++ b/docs/diagrams/Tokenizer.puml @@ -0,0 +1,41 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +package "Storage" { + interface Tokenizer { + + tokenize(ArrayList) + + detokenize(ArrayList) + } + + class AtomicHabitTokenizer { + - splitTokenizedHabitIntoParameter(String) + - convertToBase(String) + - removeDuplicatedHabits(ArrayList) + - parseTokenizedHabit(String) + + tokenize(ArrayList) + + detokenize(ArrayList) + } + + class ReflectionTokenizer { + - getTokenizedIndexes(String, Set) + - splitParameter(String, String) + - splitTokenizedIndex(String) + - validateTokenizedIndexFormat(ArrayList, int, String) + - getSet(String, String) + + tokenize(ArrayList>) + + detokenize(ArrayList) + } + + class GamificationTokenizer { + + tokenize(ArrayList) + + detokenize(ArrayList) + } +} + +Tokenizer <|-- AtomicHabitTokenizer +Tokenizer <|-- ReflectionTokenizer +Tokenizer <|-- GamificationTokenizer +@enduml diff --git a/docs/diagrams/UiComponent.png b/docs/diagrams/UiComponent.png new file mode 100644 index 0000000000..6f05e1c965 Binary files /dev/null and b/docs/diagrams/UiComponent.png differ diff --git a/docs/diagrams/UiComponent.puml b/docs/diagrams/UiComponent.puml new file mode 100644 index 0000000000..ca607993e2 --- /dev/null +++ b/docs/diagrams/UiComponent.puml @@ -0,0 +1,40 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR +skinparam classAttributeIconSize 0 + +class TextUi{ +- scanner: Scanner +- separator: String ++ getCommands(): String ++ printOutMessage(): void ++ printErrorFor: void +} + +class FocusUi{ ++ isBlocking(Session session): boolean +} + +class AtomicHabitUi{ ++ printLogo(): void +} + +class GamificationUi{ ++ printLogo(): void +} + +class ReflectUi{ ++ {static} printCelebrateLevelUp() : void +} + +FocusUi --|> TextUi +AtomicHabitUi --|> TextUi +ReflectUi --|> TextUi +GamificationUi --|> TextUi + + +@enduml diff --git a/docs/diagrams/WellNusStructure.png b/docs/diagrams/WellNusStructure.png new file mode 100644 index 0000000000..bead085736 Binary files /dev/null and b/docs/diagrams/WellNusStructure.png differ diff --git a/docs/diagrams/WellnusSequence.png b/docs/diagrams/WellnusSequence.png new file mode 100644 index 0000000000..a8598b5d72 Binary files /dev/null and b/docs/diagrams/WellnusSequence.png differ diff --git a/docs/diagrams/WellnusSequence.puml b/docs/diagrams/WellnusSequence.puml new file mode 100644 index 0000000000..4f3f9cb759 --- /dev/null +++ b/docs/diagrams/WellnusSequence.puml @@ -0,0 +1,35 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR +hide footbox + +participant ":WellNus" as WellNus MODEL_COLOR +participant ":MainManager" as MainManager MODEL_COLOR +participant ":FeatureManager" as FeatureManager MODEL_COLOR +participant ":Command" as Command MODEL_COLOR + +[-> WellNus : start() +activate WellNus +WellNus -> MainManager: runEventDriver() +activate MainManager +MainManager -> FeatureManager: runEventDriver() +activate FeatureManager +loop not home command +FeatureManager -> Command : execute() +activate Command +Command --> FeatureManager +deactivate Command +Command -[hidden]-> FeatureManager +destroy Command +Command -[hidden]-> FeatureManager +end +FeatureManager --> MainManager : 'home' command given +deactivate FeatureManager +MainManager --> WellNus : 'exit' command given +deactivate MainManager +[<--WellNus +deactivate WellNus +@enduml diff --git a/docs/diagrams/git_command.png b/docs/diagrams/git_command.png new file mode 100644 index 0000000000..583359da00 Binary files /dev/null and b/docs/diagrams/git_command.png differ diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml new file mode 100644 index 0000000000..7a60cf66b7 --- /dev/null +++ b/docs/diagrams/style.puml @@ -0,0 +1,56 @@ +/' + 'Code is adopted from https://github.com/se-edu/addressbook-level3/blob/master/docs/diagrams/style.puml + 'Commonly used styles and colors across diagrams. + 'Refer to https://plantuml-documentation.readthedocs.io/en/latest for a more + 'comprehensive list of skinparams. + '/ + + +'T1 through T4 are shades of the original color from lightest to darkest + +!define MODEL_COLOR #9D0012 +!define MODEL_COLOR_T1 #F97181 +!define MODEL_COLOR_T2 #E41F36 +!define MODEL_COLOR_T3 #7B000E +!define MODEL_COLOR_T4 #51000A + +!define USER_COLOR #FFFFFF + +skinparam BackgroundColor #FFFFFFF +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle +skinparam Shadowing false +skinparam classAttributeIconSize 0 + +skinparam Class { + FontColor #FFFFFF + BorderThickness 1 + BorderColor #FFFFFF + StereotypeFontColor #000000 + FontName Arial +} + +skinparam Actor { + BorderColor USER_COLOR + Color USER_COLOR + FontName Arial +} + +skinparam Sequence { + MessageAlign center + BoxFontSize 15 + BoxPadding 0 + BoxFontColor #FFFFFF + FontName Arial +} + +skinparam Participant { + FontColor #FFFFFF + Padding 20 +} + +'hide footbox +hide circle + diff --git a/docs/team/bernard.jpg b/docs/team/bernard.jpg new file mode 100644 index 0000000000..ad9c667e55 Binary files /dev/null and b/docs/team/bernard.jpg differ diff --git a/docs/team/bernardlesley.md b/docs/team/bernardlesley.md new file mode 100644 index 0000000000..301c7ed28c --- /dev/null +++ b/docs/team/bernardlesley.md @@ -0,0 +1,57 @@ +## Bernard Lesley Efendy - Project Portfolio Page + +### Project: WellNUS++ +WellNUS++ is a Command Line Interface(CLI) app for NUS Computing students to keep track and improve their physical and +mental wellness in various aspects. + +### Summary of Contributions +- **Code Contributions:** [Link to reposense contribution](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=BernardLesley&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-02-17). +- **Feature:** `AtomicHabitTokenizer` + [#151](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/151). + - **What:** `AtomicHabitTokenizer` is responsible for the following function: + 1. Tokenize list of `AtomicHabit` objects into Strings that can be stored. + 2. Detokenize the Strings back into list of `AtomicHabit` objects to be used by `AtomicHabitManager`. + 3. Check if the contents of `habit.txt` has been tampered/corrupted. + - **Justification:** Since `AtomicHabit` objects cannot be stored directly into the storage, there is a need for a class to tokenize and detokenize AtomicHabit objects. +- **Feature:** `ReflectionTokenizer` + [#151](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/151). + - **What:** `ReflectionTokenizer` is responsible for the following function: + 1. Tokenize like and prev indexes into Strings that can be stored. + 2. Detokenize the Strings back into like and prev indexes to be used by `ReflectionManager`. + 3. Check if the contents of `reflect.txt` has been tampered/corrupted. + - **Justification:** Since `Reflection` feature needs to save 2 types of indexes (like and prev), there is a need for a class to tokenize and detokenize like and prev indexes. +- **Feature:** WellNUS++ `Atomic Habit`, `Reflection`, `Focus Timer` and `Gamification` - `help` command implementation, [#175](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/175). +- **Feature:** Atomic Habit - `update` & `delete` command implementation, [#59](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/59) [#196](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/196). +- **User Guide Contributions:**
+ Bernard added documentation for the following sections of the user guide: + - [Delete Atomic Habit]( + https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#delete---delete-an-atomic-habit + ) + - [Viewing Help Atomic Habit]( + https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#help---viewing-atomic-habit-help + ) + - [Viewing Help Reflection]( + https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#help---viewing-reflection-help + ) + - [Viewing Help Focus Timer]( + https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#help---viewing-focus-timer-help + ) + - [Viewing Help Gamification]( + https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#help---viewing-gamification-help + ) +- **Developer Guide Contributions:**
+Bernard added documentation for the following sections of the developer guide: + - [Tokenizer]( + https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#tokenizer + ) + - [Tokenizer Class Diagram]( + https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/Tokenizer.png + ) +- **Team-Based Task and Review Contributions**
+Bernard also contributed in the following tasks: + - Attend weekly meetings to discuss the implementation of WellNUS++. + - **Finding bugs for WellNUS++**: [#298](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/298) [#317](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/317) [#321](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/321) [#323](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/323) +- **Community Contributions**
+Bernard also contributed to other teams by reviewing other team's UG and Program during PE-D and PE. + - **Reviewing other team's UG and Program**: [List of all 9 issues filed for T15-1](https://github.com/BernardLesley/ped/issues) + diff --git a/docs/team/haoyangw.md b/docs/team/haoyangw.md new file mode 100644 index 0000000000..d364f4c75f --- /dev/null +++ b/docs/team/haoyangw.md @@ -0,0 +1,127 @@ +# Wang Haoyang's Project Portfolio Page + +## Project: WellNUS++ +WellNUS++ is a Command Line Interface(CLI) app for NUS Computing students to keep track and improve their physical and +mental wellness in various aspects. It is written in Java, and has about 10 kLoC. + +### Summary of Contributions +Haoyang's main roles are to peer review team code contributions, assist with bug fixing, and write quality +documentation. He has consistently enforced code style guidelines(based on the +[SE-EDU style guide](https://se-education.org/guides/conventions/java/basic.html)) in team code. +* **Code contributed**: [RepoSense link](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=haoyangw&breakdown=true) +* **New feature**: `gamification` feature + [#178](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/178) [#265](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/265) + * What it does: Provides the user with experience points and levels + * Justification: Increase XP points and levels for completing atomic habits to incentivize the user to keep doing so + * Highlights: This feature integrates with atomic habits and thus needs careful implementation to reduce coupling + between multiple classes within the two features. It was also challenging to fulfil SRP which requires gamification + data logic to be abstracted from other classes and encapsulated within one class. +* **New feature**: `MainManager` implementation to provide the main CLI user interface + [#65](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/65) + * Highlights: Highly extensible(support new features simply by updating `setSupportedFeatureManagers()`) +* **New feature**: `help` command implementation and architecture for other commands to provide `help` description + [#30](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/30) +* **New feature**: `exit` command to quit the app [#30](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/30) +* **New feature**: `Command` abstract class to define the app's user command architecture + [#29](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/29) +* **New feature**: `Tokenizer` interface to define the architecture for app data -> String and vice versa conversion + [#137](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/137) [#147](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/147) +* **Enhancements to existing features**: + * Redirect all logging to a log file to clean up CLI [#272](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/272) + [#344](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/344) + * Highlights: `Singleton` paradigm to ensure one shared `FileHandler` in the entire app. Auto wipes log file when + file size > 5MB. + * Refactor atomic habits feature to use `Command` and `Manager` abstract class and improve associated unit tests + [#72](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/72) + * Fix app crash in `reflect` feature [#86](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/86) + * JUnit Testing [#72](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/72) + [#350](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/350) + [#363](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/363) + * General bug fixing [#203](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/203) + [#207](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/207) + [#267](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/267) + [#269](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/269) + [#286](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/286) + [#343](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/343) + [#353](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/353) + * General debugging [#139](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/139) + [#155](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155#issuecomment-1479317735) + [#157](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/157) + [#256](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/256) + [#261](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/261) + [#278](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/278) + [#279](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/279) + * Refactor main `WellNus` class for more OOP [#65](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/65) +* **Documentation**: + * User Guide: + * Added documentation for gamification feature [#200](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/200) + * Make explanations clearer and fix inconsistent formatting + [#280](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/280) + [#348](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/348) + * Better FAQs [#283](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/283) + * Developer Guide: + * Added design considerations and lifecycle details for `Manager` classes [#158](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/158) + [#184](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/184) + * Added design considerations for `gamification` feature + [#340](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/340) + * Provide test cases for `Saving Data` section [#293](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/293) +* **Contributions to team-based tasks**: + * Setup checkstyle for the project [#90](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/90) + * Add new GitHub issue template for enhancements [#57](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/57) + * Setup GitHub repo permissions to enforce forking workflow [#13](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/13) + * Setup gradle for creating full-fat jar of project [#91](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/91) + * Detailed PR reviews(see below) + * Creating GitHub issues for suggested improvements/bugs + * Closing team GitHub issues + * Creating GitHub milestone v2.1 + * Closing GitHub milestone v2.0 + * Publishing git tag v1.0 and v2.0 and corresponding GitHub releases +* **Review/Mentoring contributions**: + * Code quality reviews: [#19](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/19#pullrequestreview-1331066165) + [#27](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/27#pullrequestreview-1333065611) + [#31](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/31#pullrequestreview-1333329196) + [#35](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/35#pullrequestreview-1339014294) + [#151](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/151#pullrequestreview-1352440143) + [#155](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155#pullrequestreview-1352217126) + [#253](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/253#pullrequestreview-1370261026) + [#274](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/274#pullrequestreview-1374373316) + * Code architecture review: [#33](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/33#pullrequestreview-1334183073) + * Logic/implementation reviews: [#77](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/77#pullrequestreview-1343698960) + [#121](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/121#pullrequestreview-1345355927) + [#164](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/164#pullrequestreview-1357941910) + [#179](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/179#pullrequestreview-1361712239) + [#260](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/260#pullrequestreview-1370332618) + * Exception handling/assertion reviews: [#140](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/140#pullrequestreview-1348134976) + [#146](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/146#pullrequestreview-1348716347) + [#164](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/164#pullrequestreview-1357941910) + * Team DG reviews: [#153](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/153#pullrequestreview-1352384116) + [#154](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/154#pullrequestreview-1352507615) + * Team UML reviews: [#152](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/152#pullrequestreview-1350978626) + [#153](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/153#pullrequestreview-1352580505) + [#157](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/157) + * Help teammate with debugging Java `Thread` logic and teach him to use IntelliJ Profiler [#155](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155/commits/3de219e27ebfabd3135ed3923489e52efb9cae4e) + * Team discussion on static variables and improving `reflect` feature implementation [#85](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/85) + * Proposal to further reduce static variable use [#148](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/148) + * Teach teammate to use more OOP in `reflect` feature [#164](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/164#pullrequestreview-1357941910) + * Teach team to set up checkstyle config locally [#90](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/90) + * Teach teammates how to use RepoSense `@@author` tags [#129](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/129) + * Teach teammates about git commands like `stash`, `rebase -i` and `patch` +* **Community**: + * Contributed to forum: [#47](https://github.com/nus-cs2113-AY2223S2/forum/issues/47) + * Other teams' DG reviewed: [DG #1](https://github.com/nus-cs2113-AY2223S2/tp/pull/46#pullrequestreview-1364294694), + [DG #2](https://github.com/nus-cs2113-AY2223S2/tp/pull/52#pullrequestreview-1364319607) + * Other teams' UG reviewed: [W15-4 #105](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/105), + [W15-4 #118](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/118) + * Other teams' code reviewed: [W15-4 #108](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/108), + [W15-4 #114](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/114), + [W15-4 #125](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/125), + [W15-4 #127](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/127), + [W15-4 #132](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/132), + [W15-4 #135](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/135), + [W15-4 #138](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/138), + [W15-4 #141](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/141), + [W15-4 #145](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/145), + [W15-4 #147](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/147), + [W15-4 #149](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/149), + [W15-4 #151](https://github.com/AY2223S2-CS2113-W15-4/tp/issues/151) + * Summary/Proof of aforementioned UG/code reviews: [haoyangw/ped](https://github.com/haoyangw/ped/issues) diff --git a/docs/team/haoyangw.png b/docs/team/haoyangw.png new file mode 100644 index 0000000000..84ce4b7547 Binary files /dev/null and b/docs/team/haoyangw.png differ diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/nichyjt.jpg b/docs/team/nichyjt.jpg new file mode 100644 index 0000000000..234cb365e2 Binary files /dev/null and b/docs/team/nichyjt.jpg differ diff --git a/docs/team/nichyjt.md b/docs/team/nichyjt.md new file mode 100644 index 0000000000..68ee7336f7 --- /dev/null +++ b/docs/team/nichyjt.md @@ -0,0 +1,98 @@ +## Yek Jin Teck, Nicholas - Project Portfolio Page + +### Project: WellNUS++ +WellNUS++ is a Command Line Interface(CLI) app for NUS Computing students to keep track and improve their physical and +mental wellness in various aspects. + +### Summary of Contributions +- **Code Contributions:** [Link to reposense contribution](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-02-17&tabOpen=true&tabType=authorship&tabAuthor=nichyjt&tabRepo=AY2223S2-CS2113-T12-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false). +- **Feature:** `CommandParser` + [#15](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/15), + [#19](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/19). + - **What:** Dictates the syntax of the commands + and how the program unpacks user inputs into its various components. + - **Justification:** After researching on the + [usability, design and justification](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#design-considerations-1) + of different CLI syntax forms, the `unix`-like syntax was chosen as it was the most user-friendly and maintainable. +- **Boilerplate Feature:** `Manager`, the core boilerplate that + all sub-features are built off. + [#22](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/22), + [ #33](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/33). +- **Feature:** `Storage`, built the interface for developers to store and load data + [#134](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/134), + [#140](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/140). + - **Highlights:** The design of storage ensures usability for any arbitrary feature + that requires different data structures to be saved to file. +- **Feature:** Focus Timer `config` command implementation, + [#165](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/165), + [#169](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/169). +- **Feature:** MainManager - `help` command implementation, [#104](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/104). +- **Enhancement:** Terminal Caret - an customizable & user-friendly caret, + [#79](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/79), + [#258](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/258). + - **What**: Adds a caret to the start of the terminal screen, + similar to what you'd see in any shell-based terminal like `zsh, cmd, bash`. + For example: `(reflect):~$`. + - **[Design Justification](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/79)**: + Feedback from the [PE-D](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/235) and peers showed that + they were often lost within the features. Further, the lack of a visual signifier made it ambiguous to users + to know when they should input commands. + - **Highlights**: Terminal caret unambiguously shows where the user is, and allows users to feel + comfortable as it mimics a terminal shell which they are familiar with. +- **Enhancement:** Focus Timer - Streamlining state management, + [#169](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/169). + Created state-management methods, enforcing SRP with only `Session` directly querying and handling timer state. + - **What:** The first focus timer implementation lacked proper state management. + It was hard to figure out what commands can be run (e.g. `resume` cannot be run before `pause`). + - **Highlights:** Without changing the core logic, a developer-friendly API was made to make state management easier. + **Design & justification** are in the + [Developer Guide](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#focus-timer-implementation). +- **User Guide Contributions:** + The user guide structure was [templated from](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/108#issue-1627844297) + Nicholas' [ip User Guide](https://nichyjt.github.io/ip/). + Apart from proofreading the document, Nicholas added documentation for the following sections of the user guide: + - [Command Format](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#command-format) + - [Focus Timer Access](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#ft---accessing-focus-timer-feature) + - [Focus Timer Config Command](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#config---configure-the-timer) + - [Command Summary](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#command-summary) +- **Developer Guide Contributions:** + Nicholas added documentation and diagrams for the following sections of the developer guide, focussing on + readability and simplicity: + - [CommandParser](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#commandparser-component), and + its [Class diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/CommandParserClass.png) and + [Sequence diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/CommandParserSequence.png) + - [Storage](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#storage) + and its [Sequence diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/StorageSequence-Saving_Data__Emphasis_on_Storage_Subroutine_.png) + - [FocusTimer](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#focus-timer-component) + and its [Class diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/FocusTimerClassDiagram.png) + and [abridged 'Finite State Machine' diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/FocusTimerState.png) + - [Glossary](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#glossary) +- **Team-Based Task Contributions:** + - Setting up of [issue templates, #4](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/4) + and issue tags + - [Issue creation, tracking and closing](https://github.com/AY2223S2-CS2113-T12-4/tp/issues?q=is%3Aissue+involves%3Anichyjt) + - Took meeting minutes for meetings & booking & setup of weekly meeting venue +- **Reviewing/Mentoring Contributions** + - [List of all reviewed PRs](https://github.com/AY2223S2-CS2113-T12-4/tp/pulls?q=is%3Apr+reviewed-by%3Anichyjt). + Nicholas focussed on giving critical suggestions and on the quality of + the codebase. Some illustrative examples: + - [Avoiding deep nesting](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155#discussion_r1144643398) + - [Usage of assertions and try-catch](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/76#discussion_r1136795952) + - [Leveraging on hidden method opportunity](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/27#discussion_r1131190083) + - [Advising against dangerous use of static keyword](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/35#discussion_r1133057443) + - [Refactor suggestion for maintainability](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155#discussion_r1144648259) + - [Refactor suggestion for performance & maintainability](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155#discussion_r1144683078) + - [Discussion with team on static keyword](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/85#issuecomment-1471569085) + - [Code style consistency](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/65#discussion_r1134946097) +- **Mentoring** + - Taught the team informally through telegram to set up code auto-formatting on save + - Informally debugged and acted as a rubber duck for teammates on technical issues via Telegram +- **Community Contributions** + - **Forum Contributions:** [Object-oriented programming](https://github.com/nus-cs2113-AY2223S2/forum/issues/24#issuecomment-1417417500) + theory, [Code design](https://github.com/nus-cs2113-AY2223S2/forum/issues/34#issuecomment-1463563460) and + [IDE help](https://github.com/nus-cs2113-AY2223S2/forum/issues/34#issuecomment-1463563460). + - **Reviewing other team's DG:** [DG #1](https://github.com/nus-cs2113-AY2223S2/tp/pull/14#discussion_r1152711554), + [DG #2](https://github.com/nus-cs2113-AY2223S2/tp/pull/14#discussion_r1152715587), + [DG #3](https://github.com/nus-cs2113-AY2223S2/tp/pull/14#discussion_r1152717757), + [DG #4](https://github.com/nus-cs2113-AY2223S2/tp/pull/14#discussion_r1152731276) + - **Reviewing other team's UG and Program**: [List of all 19 issues filed for T15-3](https://github.com/nichyjt/ped/issues) \ No newline at end of file diff --git a/docs/team/wenxin-c.md b/docs/team/wenxin-c.md new file mode 100644 index 0000000000..f08e05b07c --- /dev/null +++ b/docs/team/wenxin-c.md @@ -0,0 +1,93 @@ +# Wenxin's Project Portfolio Page + +## Project: WellNUS++ +WellNUS++ is a Command Line Interface(CLI) app for NUS Computing students to keep track and improve their physical and +mental wellness in various aspects. + +### Summary of contributions +- **New Feature 1**: `TextUi` class to read user input and print output. +[#27](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/27) [#48](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/48) + - What it does: Read user inputs from terminal and print output with a user-friendly layout. + - Justification: It allows users to input commands and receive outputs from WellNUS++. + - Highlights: Command usages are given with error message to guide users. +- **New Feature 2**: Self Reflection `ReflectionManager` implementation. [#35](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/35) + - What it does: main event driver of Self Reflection section. +- **New Feature 3**: Self Reflection `get` command implementation. [#35](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/35) + - What it does: Allows users to get a **random set of 5 reflection questions** to reflect on. + - Justification: It allows users to think and reflect on themselves. The set of questions is designed to be randomised + so that users can reflect on different aspects + of lives. + - Highlights: Multiple data structures such as **ArrayList** and **Set** are used to randomise the sets of questions. +- **New Feature 4**: Self Reflection `like` command implementation. [#164](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/164) + - What it does: Allows users to add reflection questions they resonate well into favorite list for review in the future. + - Highlights: A **HashMap** is used with **display index** of questions being the **key** and **real question index** + being the **value** to match questions. +- **New Feature 5**: Self Reflection `fav` command implementation. [#164](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/164) + - What it does: Allows users to list all reflection questions in the favorite list. +- **New Feature 6**: Self Reflection `home` command to return back main WellNUS++. +[#35](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/35) [#103](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/103) +- **New Feature 7**: Helped in Self Reflection `prev` command implementation. +- **Enhancement 1**: Abstract `QuestionList` class. [#164](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/164) + - What it does: A `QuestionList` class is abstracted to store and modify user data (e.g. the random sets and favorite list). + - Implementation: To centralise and share data among classes, a common `QuestionList` object is passed into different command objects. + As such, the single responsibility principle can be better achieved. +- **Enhancement 2**: Self Reflection `unlike` command implementation. [#253](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/253) + - What it does: Allows users to remove reflection questions they no longer resonate from the favorite list to maintain the relevancy + and size of the favorite list. +- **Enhancement 3**: Store Self Reflection data in data file. [#172](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/172) + - Justification: To ensure the usage of WellNUS++ in the long run. +- **Enhancement 4**: Standardize error message across WellNUS++. [#260](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/260) + - Implementation: A standardised format of error message is used across WellNUS++. The messages pinpoint the specific +error made and give the correct command usage for users. +- **Code Contributed:** [RepoSense Link](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=wenxin-c&breakdown=true) +- **Project Management:** + - Set up the GitHub team organisation and repository. + - Maintain issue tracker and PR review. + - Team lead: responsible for the overall project coordination, defining, assigning, and tracking project tasks. + - In charge of the program and testing of TextUi class and Self Reflection section. + - Enable assertion in gradle file. [Enable Team Assertion](https://github.com/AY2223S2-CS2113-T12-4/tp/issues/141) +- **Community:** + - PRs reviewed(with non-trivial comments): +[#31](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/31), +[#65](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/65), +[#72](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/72), +[#151](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/151), +[#252](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/252) + - Reported bugs and suggestions to other teams: +[#66](https://github.com/AY2223S2-CS2113-T15-4/tp/issues/66), [#72](https://github.com/AY2223S2-CS2113-T15-4/tp/issues/72), +[#83](https://github.com/AY2223S2-CS2113-T15-4/tp/issues/83), [#92](https://github.com/AY2223S2-CS2113-T15-4/tp/issues/92) +[#103](https://github.com/AY2223S2-CS2113-T15-4/tp/issues/103), [#106](https://github.com/AY2223S2-CS2113-T15-4/tp/issues/106) +[#DG1](https://github.com/nus-cs2113-AY2223S2/tp/pull/15/files#diff-1a95edf069a4136e9cb71bee758b0dc86996f6051f0d438ec2c424557de7160b), +[#DG2](https://github.com/nus-cs2113-AY2223S2/tp/pull/3/files/6539d4f8311a3ce7587eae50de850c64e742f2a3#diff-1a95edf069a4136e9cb71bee758b0dc86996f6051f0d438ec2c424557de7160b), +[#DG3](https://github.com/nus-cs2113-AY2223S2/tp/pull/5/files/e3180a6667d0623ba95e1212667ebf9afc4ecbc1#diff-1a95edf069a4136e9cb71bee758b0dc86996f6051f0d438ec2c424557de7160b) +- **Tools:** + - Integrated PlantUML into team repo +- **Documentations:** + - **User Guide:** + - Added [WellNUS++ structure overview](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#overview-of-wellnus) + and structure diagram [#263](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/263/)
+ - Added [Access Self Reflection](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#reflect---accessing-self-reflection-feature) + documentation [#198](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/198/) + - Added Self Reflection documentation for [like](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#like---add-reflection-question-into-favorite-list), + [fav](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#fav---view-favorite-list) and + [prev](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#prev---get-the-previous-set-of-reflection-questions-generated) + [#198](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/198/) + - Added Self Reflection documentation for [unlike](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#unlike---remove-questions-from-favorite-list) + command [#253](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/253) + - Adjusted cosmetic and errors [#206](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/206) [#263](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/263) + - **Developer Guide:** + - Set up initial developer guide [#144](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/144) + - Added [UI Class Diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/UiComponent.png) + and [Implementation Details](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#ui-implementation) + [#291](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/291) + - Added [Reflection Class Diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/ReflectionClassDiagram.png) + and [Implementation Details](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#self-reflection-implementation) + [#153](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/153) + - Added [Reflection Sequence Diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/ReflectionSequenceDiagram.png) + [#181](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/181) + [#183](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/183) + - Fixed Reflection class and sequence diagram errors [#268](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/268/) + - Restructured Reflection documentation and + added [Design Considerations](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#design-considerations) + [#291](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/291)
+ \ No newline at end of file diff --git a/docs/team/wenxin.jpg b/docs/team/wenxin.jpg new file mode 100644 index 0000000000..671023fe04 Binary files /dev/null and b/docs/team/wenxin.jpg differ diff --git a/docs/team/yongbin.png b/docs/team/yongbin.png new file mode 100644 index 0000000000..d3e8521b79 Binary files /dev/null and b/docs/team/yongbin.png differ diff --git a/docs/team/yongbinwang.md b/docs/team/yongbinwang.md new file mode 100644 index 0000000000..e06ab3da9d --- /dev/null +++ b/docs/team/yongbinwang.md @@ -0,0 +1,69 @@ +## Wang Yongbin - Project Portfolio Page + +### Project: WellNUS++ + +WellNUS++ is a Command Line Interface(CLI) app for NUS Computing students to keep track and improve their physical and +mental wellness in various aspects. + +### Summary of Contributions + +- **Code Contributions:** [Link to reposense contribution graph](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-02-17&tabOpen=true&tabType=authorship&tabAuthor=YongbinWang&tabRepo=AY2223S2-CS2113-T12-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false). +- **New Feature:** `AtomicHabit` Conceptualised and built the main implementation of `AtomicHabit` feature, + [#31](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/31). + - **What it does:** Allows users to track their atomic habits to inculcate good habits. + - **Implemented:** `AtomicHabitsManager` , `AtomicHabit`,`AtomicHabitList`, `AddCommand`, `ListCommand`,`HomeCommand`. +- **New Feature**: `Focus Timer` Conceptualised and built the main implementation of `FocusTimer` feature through + experimenting with different implementation approaches, + [#155](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155). + - **What it does:** Users can start a offline focus session to focus on their task. + - **Implemented:** `FocusTimerManager`, `Session`, `StartCommand`, `CheckCommand`, `PauseCommand`, `ResumeCommand`, `NextCommand`, `StopCommand`. + - **Justification:** The `FocusTimer` feature allows users to have a Work/Break timer that is + offline and does not require internet connection, minimising online distractions. +- **New Feature:** `Countdown`, + [#155](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/155). + - **What it does:** Simulates a counting down timer in the command line. + - **Justification:** A versatile class is required to implement different types of timers for the feature to be robust. + - **Highlights:** This feature required out of topic research on background threads through the use of `Timer` and `TimerTask` classes. + Atomic data types were used to ensure thread safety. The implementation of `FocusTimer` was challenging as there was a + need to ensure no bugs were introduced into the codebase while implementing a novel concept. +- **Enhancement:** Duplicate Checking - Implemented duplicate checking for user inputs, + [#259](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/259). + - **What it does:** Checks previously inputted data to ensure that the user does not add duplicate data. + - **Justification:** Accidental input of duplicate data can be frustrating for the user. +- **Enhancement:** Improve User Interface - Implemented duplicate checking for user inputs, + [#274](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/274). + - **What it does:** Improves UI for `AtomicHabit` and `FocusTimer` features. + - **Justification:** User will have a better experience and can easily differentiate between different features. +- **User Guide Contributions:** + Yongbin set up the initial structure of the user guide and added documentation for the following sections of the user + guide: + - [Add new atomic habit](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#add---add-new-atomic-habit) + - [List all atomic habit](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#list---list-all-atomic-habit) + - [Start Session](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#start---start-session) + - [Pause Session](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#pause---pause-session) + - [Resume Session](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#resume---resume-session) + - [Check Session](https://ay2223s2-cs2113-t12-4.github.io/tp/UserGuide.html#check---check-time) +- **Developer Guide Contributions:** + Yongbin added documentation for the following sections of the developer guide: + - [Atomic Habit](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#atomichabit-component), and + its [Class diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/AtomicHabit.png) and + [Sequence diagram](https://ay2223s2-cs2113-t12-4.github.io/tp/diagrams/AtomicHabitSequenceDiagram.png) + - [Focus Timer](https://ay2223s2-cs2113-t12-4.github.io/tp/DeveloperGuide.html#commands-1) +- **Team-Based Task Contributions:** + - Noting down important matters discussed during meetings. + - Clarifying certain misconceptions in group chat. + - PR reviews. +- **Reviewing/Mentoring Contributions** + - Yongbin was involved in reviewing PRs pertaining to Focus Timer/Atomic Habit features. As well as key features for the + application such as storage and manager. + - [List of all reviewed PRs](https://github.com/AY2223S2-CS2113-T12-4/tp/pulls?q=is%3Apr+reviewed-by%3AYongbinWang) + - [Add config and restart logic for FocusTimer](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/169) + - [Refactor atomic habits feature ](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/72) + - [Add Manager class](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/33) + - [Feature-Update Atomic Habit](https://github.com/AY2223S2-CS2113-T12-4/tp/pull/59) +- **Mentoring** + - Helped to clarify certain misconceptions on application architecture and integration of different features. +- **Community Contributions** + - **Reviewing other team's DG:** [DG #1](https://github.com/nus-cs2113-AY2223S2/tp/pull/12), + [DG #2](https://github.com/nus-cs2113-AY2223S2/tp/pull/42) + - **Reviewing other team's UG and Program**: [List of all 15 issues filed for T11-3](https://github.com/YongbinWang/ped/issues) \ No newline at end of file diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/main/java/wellnus/WellNus.java b/src/main/java/wellnus/WellNus.java new file mode 100644 index 0000000000..529b9cb8c0 --- /dev/null +++ b/src/main/java/wellnus/WellNus.java @@ -0,0 +1,90 @@ +package wellnus; + +import wellnus.common.MainManager; +import wellnus.manager.Manager; +import wellnus.ui.TextUi; + +/** + * Main class of our WellNUS++ application. main() is executed when the application is launched.
+ *

+ * Control is then passed to MainManager.runEventDriver(). + * + * @see MainManager#runEventDriver() + */ +public class WellNus { + private static final String BYE_MESSAGE = "Thank you for using WellNUS++! See you again soon Dx"; + private static final String GREETING_MESSAGE = "Very good day to you! Welcome to "; + private static final String NEWLINE = System.lineSeparator(); + private final TextUi textUi; + private final MainManager mainManager; + + /** + * Initialises an instance of WellNUS++, which needs TextUi + * and MainManager. + */ + public WellNus() { + this.textUi = new TextUi(); + this.mainManager = new MainManager(); + } + + private static String getWellNusLogo() { + return NEWLINE + + ",--. ,--. ,--.,--.,--. ,--.,--. ,--. ,---. | | | | " + NEWLINE + + "| | | | ,---. | || || ,'.| || | | |' .-',---| |---.,---| |---. " + NEWLINE + + "| |.'.| || .-. :| || || |' ' || | | |`. `-.'---| |---''---| |---' " + NEWLINE + + "| ,'. |\\ --.| || || | ` |' '-' '.-' | | | | | " + NEWLINE + + "'--' '--' `----'`--'`--'`--' `--' `-----' `-----' `--' `--' " + NEWLINE; + } + + private void byeUser() { + this.getTextUi().printOutputMessage(WellNus.BYE_MESSAGE); + } + + /** + * Calls MainManager to read and execute the user's commands. + * + * @see Manager#runEventDriver() + */ + private void executeUserCommands() { + this.getMainManager().runEventDriver(); + } + + private MainManager getMainManager() { + return this.mainManager; + } + + private TextUi getTextUi() { + return this.textUi; + } + + private void greet() { + this.getTextUi().printOutputMessage(WellNus.GREETING_MESSAGE + WellNus.NEWLINE + + WellNus.getWellNusLogo()); + } + + /** + * Executes the WellNus application and provides the user with our features. + * + * @param args Commandline arguments passed to the WellNus Java ARchive + */ + public static void main(String[] args) { + new WellNus().start(); + } + + /** + * Starts up WellNUS++: Greets the user, reads for commands until a exit command is given, + * and bids the user goodbye.
+ *

+ * The bulk of the work is done in executeUserCommands(), which delegates control to the + * appropriate Manager. + * + * @see Manager#runEventDriver() + */ + public void start() { + this.greet(); + this.executeUserCommands(); + this.byeUser(); + } + +} + diff --git a/src/main/java/wellnus/atomichabit/command/AddCommand.java b/src/main/java/wellnus/atomichabit/command/AddCommand.java new file mode 100644 index 0000000000..17c1ff800b --- /dev/null +++ b/src/main/java/wellnus/atomichabit/command/AddCommand.java @@ -0,0 +1,183 @@ +package wellnus.atomichabit.command; + +import java.util.ArrayList; +import java.util.HashMap; + +import wellnus.atomichabit.feature.AtomicHabit; +import wellnus.atomichabit.feature.AtomicHabitList; +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.atomichabit.feature.AtomicHabitUi; +import wellnus.command.Command; +import wellnus.exception.AtomicHabitException; +import wellnus.exception.BadCommandException; + + +/** + * The AddCommand class is a command class that adds a new atomic habit to an AtomicHabitList.
+ */ +public class AddCommand extends Command { + public static final String COMMAND_DESCRIPTION = "add - Add a habit to your habit tracker."; + public static final String COMMAND_USAGE = "usage: add --name (your habit name)"; + public static final String COMMAND_KEYWORD = "add"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'add'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'add'!"; + private static final String COMMAND_EMPTY_NAME = "Invalid habit name given to 'add'!"; + private static final String DUPLICATE_HABIT_MESSAGE = "You already have this habit in your list!" + + " Use 'update' instead."; + private static final String COMMAND_INVALID_HABIT_NAME_MESSAGE = "Invalid habit name given to 'add'!" + + System.lineSeparator() + + "Habit name should not contain only numbers and symbols!"; + private static final String COMMAND_NAME_ARGUMENT = "name"; + private static final String COMMAND_KEYWORD_ASSERTION = "The key should be add."; + private static final int COMMAND_NUM_OF_ARGUMENTS = 2; + private static final String REGEX_NUMBER_AND_SYMBOL_ONLY_PATTERN = "^[\\d\\p{Punct}\\p{S}]*$"; + private static final String COMMAND_WRONG_KEYWORD_MESSAGE = "Invalid command issued, expected 'add'!"; + private static final String FEEDBACK_STRING_ONE = "Yay! You have added a new habit:"; + private static final String FEEDBACK_STRING_TWO = "was successfully added"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "add command " + COMMAND_USAGE; + private final AtomicHabitList atomicHabits; + private final AtomicHabitUi atomicHabitUi; + + /** + * Constructs an AddCommand object.
+ * + * @param arguments Argument-Payload map generated by CommandParser. + * @param atomicHabits The AtomicHabitList object to add the habit to. + */ + public AddCommand(HashMap arguments, AtomicHabitList atomicHabits) { + super(arguments); + this.atomicHabits = atomicHabits; + this.atomicHabitUi = new AtomicHabitUi(); + } + + private AtomicHabitList getAtomicHabits() { + return atomicHabits; + } + + private AtomicHabitUi getTextUi() { + return atomicHabitUi; + } + + private boolean hasDuplicate(String newHabit, ArrayList habitList) { + for (AtomicHabit habit : habitList) { + if (convertToBase(habit.getDescription()).equals(convertToBase(newHabit))) { + return true; + } + } + return false; + } + + private String convertToBase(String habitName) { + return habitName.toLowerCase().replaceAll("\\s", ""); + } + + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + assert COMMAND_KEYWORD != null : "COMMAND_KEYWORD cannot be null"; + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return AtomicHabitManager.FEATURE_NAME; + } + + /** + * Adds of the new atomic habit into our list of atomic habits. + *

+ * After that, print a message telling the user what the new habit added is + * + * @throws AtomicHabitException If the habit already exists in the list + */ + @Override + public void execute() throws AtomicHabitException { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + this.getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + assert super.getArguments().containsKey(COMMAND_KEYWORD) : COMMAND_KEYWORD_ASSERTION; + String name = super.getArguments().get(AddCommand.COMMAND_NAME_ARGUMENT); + if (hasDuplicate(name, atomicHabits.getAllHabits())) { + throw new AtomicHabitException(DUPLICATE_HABIT_MESSAGE); + } + AtomicHabit habit = new AtomicHabit(name); + this.getAtomicHabits().addAtomicHabit(habit); + String messageToUser = FEEDBACK_STRING_ONE + System.lineSeparator(); + messageToUser += String.format("'%s' %s", habit, FEEDBACK_STRING_TWO); + getTextUi().printOutputMessage(messageToUser); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser.
+ *
+ * If no exceptions are thrown, command is valid. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != AddCommand.COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(AddCommand.COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(AddCommand.COMMAND_KEYWORD)) { + throw new BadCommandException(AddCommand.COMMAND_WRONG_KEYWORD_MESSAGE); + } + if (arguments.get(COMMAND_KEYWORD) != "") { + throw new BadCommandException(AddCommand.COMMAND_INVALID_PAYLOAD); + } + if (!arguments.containsKey(AddCommand.COMMAND_NAME_ARGUMENT)) { + throw new BadCommandException(AddCommand.COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + String nameArg = arguments.get(AddCommand.COMMAND_NAME_ARGUMENT); + if (nameArg.equals("")) { + throw new BadCommandException(AddCommand.COMMAND_EMPTY_NAME); + } + if (nameArg.matches(REGEX_NUMBER_AND_SYMBOL_ONLY_PATTERN)) { + throw new BadCommandException(AddCommand.COMMAND_INVALID_HABIT_NAME_MESSAGE); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

+ * For example, for the 'add' command in AtomicHabit package:
+ * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

+ * For example, for the 'add' command in AtomicHabit package:
+ * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} + + diff --git a/src/main/java/wellnus/atomichabit/command/DeleteCommand.java b/src/main/java/wellnus/atomichabit/command/DeleteCommand.java new file mode 100644 index 0000000000..115f713d03 --- /dev/null +++ b/src/main/java/wellnus/atomichabit/command/DeleteCommand.java @@ -0,0 +1,204 @@ +package wellnus.atomichabit.command; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.atomichabit.feature.AtomicHabit; +import wellnus.atomichabit.feature.AtomicHabitList; +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.atomichabit.feature.AtomicHabitUi; +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.AtomicHabitException; +import wellnus.exception.BadCommandException; +import wellnus.ui.TextUi; + +/** + * The DeleteCommand class is a command class that deletes a habit + * has been preformed.
+ */ +public class DeleteCommand extends Command { + public static final String COMMAND_DESCRIPTION = "delete - Delete the habit you don't want to continue."; + public static final String COMMAND_USAGE = "usage: delete --id habit-index"; + public static final String COMMAND_KEYWORD = "delete"; + private static final String COMMAND_INDEX_ARGUMENT = "id"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 2; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'delete'"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'delete'!"; + private static final String FEEDBACK_STRING = "The following habit has been deleted:"; + private static final String FEEDBACK_STRING_TWO = "has been successfully deleted"; + private static final String FEEDBACK_INDEX_NOT_INTEGER_ERROR = "Invalid index payload given in 'delete', expected " + + "a valid integer"; + private static final String FEEDBACK_INDEX_OUT_OF_BOUNDS_ERROR = "Invalid index payload given in 'delete', " + + "index is out of range!"; + private static final String FEEDBACK_EMPTY_LIST_UPDATE = "There are no habits to delete! " + + "Please `add` a habit first!"; + private static final int INDEX_OFFSET = 1; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String DELETE_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'delete'"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "delete command " + COMMAND_USAGE; + private static final Logger LOGGER = WellNusLogger.getLogger("DeleteAtomicHabitLogger"); + private static final String LOG_STR_INPUT_NOT_INTEGER = "Input string is not an integer." + + "This should be properly handled"; + + private static final String LOG_INDEX_OUT_OF_BOUNDS = "Input index is out of bounds." + + "This should be properly handled"; + private final AtomicHabitList atomicHabits; + private final AtomicHabitUi atomicHabitUi; + + /** + * Constructs an DeleteCommand object with the given arguments and AtomicHabitList.
+ * + * @param arguments Argument-Payload map generated by CommandParser. + * @param atomicHabits The AtomicHabitList object containing habit to be deleted. + */ + public DeleteCommand(HashMap arguments, AtomicHabitList atomicHabits) { + super(arguments); + this.atomicHabits = atomicHabits; + this.atomicHabitUi = new AtomicHabitUi(); + } + + /** + * Constructs an DeleteCommand object with the given InputStream, arguments and AtomicHabitList.
+ * + * @param inputStream An InputStream object representing the input stream to be used. + * @param arguments Argument-Payload map generated by CommandParser. + * @param atomicHabits The AtomicHabitList object containing habit to be deleted. + */ + public DeleteCommand(InputStream inputStream, HashMap arguments, + AtomicHabitList atomicHabits) { + super(arguments); + this.atomicHabits = atomicHabits; + this.atomicHabitUi = new AtomicHabitUi(inputStream); + } + + private AtomicHabitList getAtomicHabits() { + return this.atomicHabits; + } + + private TextUi getTextUi() { + return this.atomicHabitUi; + } + + + private int getIndexFrom(HashMap arguments) throws BadCommandException, NumberFormatException { + if (!arguments.containsKey(COMMAND_INDEX_ARGUMENT)) { + throw new BadCommandException(DELETE_INVALID_ARGUMENTS_MESSAGE); + } + String indexString = arguments.get(COMMAND_INDEX_ARGUMENT); + return Integer.parseInt(indexString); + } + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return AtomicHabitManager.FEATURE_NAME; + } + + /** + * Executes the delete command for atomic habits.
+ *

+ * This command is interactive, so user will continue providing arguments via + * further prompts provided. + */ + @Override + public void execute() throws AtomicHabitException { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + try { + if (getAtomicHabits().getAllHabits().isEmpty()) { + getTextUi().printOutputMessage(FEEDBACK_EMPTY_LIST_UPDATE); + return; + } + int index = this.getIndexFrom(super.getArguments()) - INDEX_OFFSET; + AtomicHabit habitToDelete = getAtomicHabits().getHabitByIndex(index); + atomicHabits.deleteAtomicHabit(habitToDelete); + String stringOfDeletedHabit = habitToDelete + " " + "[" + habitToDelete.getCount() + "]" + " " + + FEEDBACK_STRING_TWO + + LINE_SEPARATOR; + getTextUi().printOutputMessage(FEEDBACK_STRING + LINE_SEPARATOR + + stringOfDeletedHabit); + } catch (NumberFormatException numberFormatException) { + LOGGER.log(Level.INFO, LOG_STR_INPUT_NOT_INTEGER); + throw new AtomicHabitException(FEEDBACK_INDEX_NOT_INTEGER_ERROR); + } catch (IndexOutOfBoundsException e) { + LOGGER.log(Level.INFO, LOG_INDEX_OUT_OF_BOUNDS); + throw new AtomicHabitException(FEEDBACK_INDEX_OUT_OF_BOUNDS_ERROR); + } catch (BadCommandException badCommandException) { + getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + } + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser.
+ *

+ * If no exceptions are thrown, arguments are valid. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (!arguments.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(COMMAND_INVALID_COMMAND_MESSAGE); + } + if (!arguments.get(COMMAND_KEYWORD).equals("")) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + if (arguments.size() != COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(DELETE_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(COMMAND_INDEX_ARGUMENT)) { + throw new BadCommandException(DELETE_INVALID_ARGUMENTS_MESSAGE); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

+ * For example, for the 'add' command in AtomicHabit package:
+ * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

+ * For example, for the 'add' command in AtomicHabit package:
+ * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + +} diff --git a/src/main/java/wellnus/atomichabit/command/HelpCommand.java b/src/main/java/wellnus/atomichabit/command/HelpCommand.java new file mode 100644 index 0000000000..4a1e28bcb2 --- /dev/null +++ b/src/main/java/wellnus/atomichabit/command/HelpCommand.java @@ -0,0 +1,198 @@ +package wellnus.atomichabit.command; + +import java.util.ArrayList; +import java.util.HashMap; + +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.atomichabit.feature.AtomicHabitUi; +import wellnus.command.Command; +import wellnus.exception.BadCommandException; + + +/** + * Implementation of Atomic Habit WellNus' help command. Explains to the user what commands are supported + * by Atomic Habit and how to use each command. + */ + +public class HelpCommand extends Command { + public static final String COMMAND_DESCRIPTION = "help - Get help on what commands can be used " + + "in Atomic Habit WellNUS++"; + public static final String COMMAND_USAGE = "usage: help [command-to-check]"; + private static final String BAD_ARGUMENTS_MESSAGE = "Invalid arguments given to 'help'!"; + private static final String COMMAND_KEYWORD = "help"; + private static final String NO_FEATURE_KEYWORD = ""; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String HELP_PREAMBLE = "Input `help` to see all available commands." + LINE_SEPARATOR + + "Input `help [command-to-check] to get usage help for a specific command." + LINE_SEPARATOR + + "Here are all the commands available for you!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'help'!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "help command " + COMMAND_USAGE; + private static final String PADDING = " "; + private static final String DOT = "."; + private static final int ONE_OFFSET = 1; + private static final int EXPECTED_PAYLOAD_SIZE = 1; + private final AtomicHabitUi atomicHabitUi; + + /** + * Initialises a HelpCommand Object using the command arguments issued by the user. + * + * @param arguments Command arguments issued by the user + */ + public HelpCommand(HashMap arguments) { + super(arguments); + this.atomicHabitUi = new AtomicHabitUi(); + } + + private AtomicHabitUi getTextUi() { + return this.atomicHabitUi; + } + + private ArrayList getCommandDescriptions() { + ArrayList commandDescriptions = new ArrayList<>(); + commandDescriptions.add(AddCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(DeleteCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(HelpCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(HomeCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(ListCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(UpdateCommand.COMMAND_DESCRIPTION); + return commandDescriptions; + } + + /** + * Prints either the general help message or the command-specific help message + * based on the presence of a payload. + */ + private void printHelpMessage() { + HashMap argumentPayload = getArguments(); + String commandToSearch = argumentPayload.get(COMMAND_KEYWORD).trim().toLowerCase(); + if (commandToSearch.equals(NO_FEATURE_KEYWORD)) { + printGeneralHelpMessage(); + return; + } + printSpecificHelpMessage(commandToSearch); + } + + /** + * Lists all features available in Atomic Habit WellNUS++ and a short description. + */ + public void printGeneralHelpMessage() { + ArrayList commandDescriptions = getCommandDescriptions(); + String outputMessage = AtomicHabitManager.FEATURE_HELP_DESCRIPTION; + outputMessage = outputMessage.concat(System.lineSeparator()); + outputMessage = outputMessage.concat(HELP_PREAMBLE); + outputMessage = outputMessage.concat(System.lineSeparator() + System.lineSeparator()); + + for (int i = 0; i < commandDescriptions.size(); i += 1) { + outputMessage = outputMessage.concat(i + ONE_OFFSET + DOT + PADDING); + outputMessage = outputMessage.concat(commandDescriptions.get(i) + System.lineSeparator()); + } + this.getTextUi().printOutputMessage(outputMessage); + } + + /** + * Prints the help message for a given commandToSearch.
+ * If the commandToSearch does not exist, help will print an unknown command + * error message. + */ + public void printSpecificHelpMessage(String commandToSearch) { + switch (commandToSearch) { + case AddCommand.COMMAND_KEYWORD: + printUsageMessage(AddCommand.COMMAND_DESCRIPTION, AddCommand.COMMAND_USAGE); + break; + case DeleteCommand.COMMAND_KEYWORD: + printUsageMessage(DeleteCommand.COMMAND_DESCRIPTION, DeleteCommand.COMMAND_USAGE); + break; + case HelpCommand.COMMAND_KEYWORD: + printUsageMessage(HelpCommand.COMMAND_DESCRIPTION, HelpCommand.COMMAND_USAGE); + break; + case HomeCommand.COMMAND_KEYWORD: + printUsageMessage(HomeCommand.COMMAND_DESCRIPTION, HomeCommand.COMMAND_USAGE); + break; + case ListCommand.COMMAND_KEYWORD: + printUsageMessage(ListCommand.COMMAND_DESCRIPTION, ListCommand.COMMAND_USAGE); + break; + case UpdateCommand.COMMAND_KEYWORD: + printUsageMessage(UpdateCommand.COMMAND_DESCRIPTION, UpdateCommand.COMMAND_USAGE); + break; + default: + BadCommandException unknownCommand = new BadCommandException(COMMAND_INVALID_PAYLOAD); + atomicHabitUi.printErrorFor(unknownCommand, COMMAND_INVALID_COMMAND_NOTE); + } + } + + private void printUsageMessage(String commandDescription, String usageString) { + String message = commandDescription + System.lineSeparator() + usageString; + atomicHabitUi.printOutputMessage(message); + } + + @Override + protected String getCommandKeyword() { + return HelpCommand.COMMAND_KEYWORD; + } + + @Override + protected String getFeatureKeyword() { + return HelpCommand.NO_FEATURE_KEYWORD; + } + + /** + * Executes the issued help command.
+ *

+ * Prints a brief description of all of Atomic Habit WellNus' supported commands if + * the basic 'help' command was issued.
+ *

+ * Prints a detailed description of a specific feature if the specialised + * 'help' command was issued. + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException exception) { + getTextUi().printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + return; + } + this.printHelpMessage(); + } + + /** + * Checks whether the given arguments are valid for our help command. + * + * @param arguments Argument-Payload map generated by CommandParser using user's command + * @throws BadCommandException If the command is invalid + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(COMMAND_KEYWORD) : "HelpCommand's payload map does not contain 'help'!"; + // Check if user put in unnecessary payload or arguments + if (arguments.size() > EXPECTED_PAYLOAD_SIZE) { + throw new BadCommandException(BAD_ARGUMENTS_MESSAGE); + } + } + + /** + * Abstract method to ensure developers add in a command usage. + *

+ * For example, for the 'add' command in AtomicHabit package:
+ * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

+ * For example, for the 'add' command in AtomicHabit package:
+ * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/atomichabit/command/HomeCommand.java b/src/main/java/wellnus/atomichabit/command/HomeCommand.java new file mode 100644 index 0000000000..6a6e18293a --- /dev/null +++ b/src/main/java/wellnus/atomichabit/command/HomeCommand.java @@ -0,0 +1,138 @@ +package wellnus.atomichabit.command; + +import java.util.HashMap; + +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.atomichabit.feature.AtomicHabitUi; +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; + +/** + * The HomeCommand class is a command class that returns user back to the main WellNUS++ program.
+ */ +public class HomeCommand extends Command { + public static final String COMMAND_DESCRIPTION = "home - Return back to the main menu of WellNUS++."; + public static final String COMMAND_USAGE = "usage: home"; + public static final String COMMAND_KEYWORD = "home"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'home'!"; + private static final String COMMAND_INVALID_ARGUMENTS = "Invalid arguments given to 'home'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'home'!"; + private static final String HOME_MESSAGE = "Thank you for using atomic habits. Do not forget about me!"; + private final AtomicHabitUi atomicHabitUi; + + /** + * Constructs an HomeCommand object.
+ * + * @param arguments Argument-Payload map generated by CommandParser. + */ + public HomeCommand(HashMap arguments) { + super(arguments); + this.atomicHabitUi = new AtomicHabitUi(); + } + + private AtomicHabitUi getTextUi() { + return this.atomicHabitUi; + } + + /** + * Check if a HomeCommand is executed and user wants to return to home. + * + * @param command User command + * @return true If user wants to exit feature false if not + */ + public static boolean isExit(Command command) { + return command instanceof HomeCommand; + } + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return AtomicHabitManager.FEATURE_NAME; + } + + /** + * Prints the exit feature message for the atomic habits feature on the user's screen. + */ + @Override + public void execute() throws WellNusException { + validateCommand(super.getArguments()); + getTextUi().printOutputMessage(HOME_MESSAGE); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser
+ *
+ * The validation logic and strictness is up to the implementer.
+ *
+ * As a guideline, isValidCommand should minimally:
+ *

  • Verify that ALL MANDATORY arguments exist
  • + *
  • Verify that ALL MANDATORY payloads exist
  • + *
  • Safely verify the payload type (int, date, etc should be properly processed)
  • + *
    + * Additionally, payload value cleanup (such as trimming) is also possible.
    + * As Java is pass (object reference) by value, any changes made to commandMap + * will persist out of the function call. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException if the commandMap has any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != HomeCommand.COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(HomeCommand.COMMAND_INVALID_ARGUMENTS); + } + if (!arguments.containsKey(HomeCommand.COMMAND_KEYWORD)) { + throw new BadCommandException(HomeCommand.COMMAND_INVALID_COMMAND_MESSAGE); + } + String payload = arguments.get(getCommandKeyword()); + if (!payload.isBlank()) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} + + diff --git a/src/main/java/wellnus/atomichabit/command/ListCommand.java b/src/main/java/wellnus/atomichabit/command/ListCommand.java new file mode 100644 index 0000000000..9321e5d810 --- /dev/null +++ b/src/main/java/wellnus/atomichabit/command/ListCommand.java @@ -0,0 +1,149 @@ +package wellnus.atomichabit.command; + +import java.util.HashMap; + +import wellnus.atomichabit.feature.AtomicHabit; +import wellnus.atomichabit.feature.AtomicHabitList; +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.atomichabit.feature.AtomicHabitUi; +import wellnus.command.Command; +import wellnus.exception.BadCommandException; + + +/** + * The ListCommand class is a command class that lists all atomic habit in AtomicHabitList.
    + */ +public class ListCommand extends Command { + public static final String COMMAND_DESCRIPTION = "list - Lists out all the habits in your tracker."; + public static final String COMMAND_USAGE = "usage: list"; + public static final String COMMAND_KEYWORD = "list"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'list'!"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'list'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'list'!"; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String FIRST_STRING = "Here is the current accumulation of your atomic habits!" + + LINE_SEPARATOR + "Keep up the good work and you will develop a helpful habit in no time"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "list command " + COMMAND_USAGE; + private static final String EMPTY_LIST_MESSAGE = "You have no habits in your list!" + + LINE_SEPARATOR + + "Start adding some habits by using 'add'!"; + private final AtomicHabitList atomicHabits; + private final AtomicHabitUi atomicHabitUi; + + /** + * Constructs an ListCommand object.
    + * + * @param arguments Argument-Payload map generated by CommandParser. + * @param atomicHabits The AtomicHabitList object to get all atomic habit. + */ + public ListCommand(HashMap arguments, AtomicHabitList atomicHabits) { + super(arguments); + this.atomicHabits = atomicHabits; + this.atomicHabitUi = new AtomicHabitUi(); + } + + private AtomicHabitUi getTextUi() { + return atomicHabitUi; + } + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return AtomicHabitManager.FEATURE_NAME; + } + + /** + * Executes the list command for atomic habits, which prints all atomic habits + * added by the user so far. + */ + @Override + public void execute() { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + this.getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + if (atomicHabits.getAllHabits().isEmpty()) { + getTextUi().printOutputMessage(EMPTY_LIST_MESSAGE); + return; + } + int taskNo = 1; + int firstChar = 0; + StringBuilder stringOfHabitsBuilder = new StringBuilder(FIRST_STRING + LINE_SEPARATOR); + for (AtomicHabit habit : atomicHabits.getAllHabits()) { + String currentHabitString = String.format("%d.%s [%d]", + taskNo, habit.toString(), habit.getCount()); + stringOfHabitsBuilder.append(currentHabitString).append(LINE_SEPARATOR); + taskNo += 1; + } + String messageToUser = stringOfHabitsBuilder.substring(firstChar, + stringOfHabitsBuilder.length() - 1); + getTextUi().printOutputMessage(messageToUser); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser.
    + *

    + * If no exceptions are thrown, arguments are valid. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the commandMap has any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != ListCommand.COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(ListCommand.COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(ListCommand.COMMAND_KEYWORD)) { + throw new BadCommandException(ListCommand.COMMAND_INVALID_COMMAND_MESSAGE); + } + if (arguments.get(COMMAND_KEYWORD) != "") { + throw new BadCommandException(ListCommand.COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_USAGE; + } +} + diff --git a/src/main/java/wellnus/atomichabit/command/UpdateCommand.java b/src/main/java/wellnus/atomichabit/command/UpdateCommand.java new file mode 100644 index 0000000000..9f621b639f --- /dev/null +++ b/src/main/java/wellnus/atomichabit/command/UpdateCommand.java @@ -0,0 +1,296 @@ +package wellnus.atomichabit.command; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.atomichabit.feature.AtomicHabit; +import wellnus.atomichabit.feature.AtomicHabitList; +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.atomichabit.feature.AtomicHabitUi; +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.AtomicHabitException; +import wellnus.exception.BadCommandException; +import wellnus.exception.StorageException; +import wellnus.gamification.util.GamificationData; +import wellnus.gamification.util.GamificationUi; + +/** + * The UpdateCommand class is a command class that updates the number of times a habit + * has been preformed.
    + */ +public class UpdateCommand extends Command { + public static final String COMMAND_DESCRIPTION = "update - Update how many times you've done a habit."; + public static final String COMMAND_USAGE = "usage: update --id habit-index [--by increment_number]"; + public static final String COMMAND_KEYWORD = "update"; + private static final String COMMAND_INCREMENT_ARGUMENT = "by"; + private static final String COMMAND_INDEX_ARGUMENT = "id"; + private static final int COMMAND_MIN_NUM_OF_ARGUMENTS = 2; + private static final int COMMAND_MAX_NUM_OF_ARGUMENTS = 3; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'update'!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "update command " + COMMAND_USAGE; + private static final String COMMAND_INVALID_ARGUMENT_MESSAGE = "Invalid arguments given to 'update'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'update'!"; + private static final String COMMAND_INVALID_PAYLOAD_INCREMENT = "Invalid payload given in 'update' " + + "command!"; + private static final String DOT = "."; + private static final int DEFAULT_INCREMENT = 1; + private static final int ZERO = 0; + private static final String FEEDBACK_INDEX_NOT_INTEGER_ERROR = "Invalid payload given in 'update' command, " + + "expected a valid integer!"; + private static final String FEEDBACK_INDEX_OUT_OF_BOUNDS_ERROR = "Invalid payload given in 'update' command, " + + "index is out of range!"; + private static final String FEEDBACK_DECREMENT_ERROR = "Invalid decrement payload given in 'update' command, " + + "decrement value is out of range!"; + private static final String FEEDBACK_STRING_INCREMENT = "The following habit has been incremented! " + + "Keep up the good work!"; + private static final String FEEDBACK_STRING_DECREMENT = "The following habit has been decremented."; + private static final String FEEDBACK_EMPTY_LIST_UPDATE = "There are no habits to update! " + + "Please `add` a habit first!"; + private static final String FEEDBACK_CHANGE_COUNT_ZERO = "Invalid count integer, updating by 0 is not allowed!"; + private static final int INDEX_OFFSET = 1; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final int MINIMUM_INCREMENT = 1; + private static final String UPDATE_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'update'!"; + private static final String UPDATE_INVALID_INCREMENT_COUNT = "Invalid increment payload given in 'update' command, " + + "increment with minimum of 1 is expected!"; + private static final String STORE_GAMIF_DATA_FAILED_NOTE_MESSAGE = "Error saving to storage!"; + private static final Logger LOGGER = WellNusLogger.getLogger("UpdateAtomicHabitLogger"); + private static final String LOG_STR_INPUT_NOT_INTEGER = "Input string is not an integer." + + "This should be properly handled"; + private static final String LOG_INDEX_OUT_OF_BOUNDS = "Input index is out of bounds." + + "This should be properly handled"; + private static final int NUM_OF_XP_PER_INCREMENT = 1; + private final AtomicHabitList atomicHabits; + private final GamificationData gamificationData; + private final AtomicHabitUi atomicHabitUi; + + /** + * Constructs an UpdateCommand object with the given arguments and AtomicHabitList.
    + * + * @param arguments Argument-Payload map generated by CommandParser. + * @param atomicHabits The AtomicHabitList object containing habit to be updates. + */ + public UpdateCommand(HashMap arguments, AtomicHabitList atomicHabits, + GamificationData gamificationData) { + super(arguments); + this.atomicHabits = atomicHabits; + this.gamificationData = gamificationData; + this.atomicHabitUi = new AtomicHabitUi(); + } + + /** + * Constructs an UpdateCommand object with the given InputStream, arguments and AtomicHabitList.
    + * + * @param inputStream An InputStream object representing the input stream to be used. + * @param arguments Argument-Payload map generated by CommandParser. + * @param atomicHabits The AtomicHabitList object containing habit to be updates. + */ + public UpdateCommand(InputStream inputStream, HashMap arguments, + AtomicHabitList atomicHabits, GamificationData gamificationData) { + super(arguments); + this.atomicHabits = atomicHabits; + this.gamificationData = gamificationData; + this.atomicHabitUi = new AtomicHabitUi(inputStream); + } + + private AtomicHabitList getAtomicHabits() { + return this.atomicHabits; + } + + private AtomicHabitUi getTextUi() { + return this.atomicHabitUi; + } + + private int getIncrementCountFrom(HashMap arguments) + throws BadCommandException, NumberFormatException { + assert arguments.containsKey(UpdateCommand.COMMAND_INCREMENT_ARGUMENT) + : "--by argument missing for 'hb update' command"; + String incrementCountString = arguments.get(UpdateCommand.COMMAND_INCREMENT_ARGUMENT); + if (Integer.parseInt(incrementCountString) < MINIMUM_INCREMENT + && isPositive(Integer.parseInt(incrementCountString))) { + throw new BadCommandException(UpdateCommand.UPDATE_INVALID_INCREMENT_COUNT); + } + return Integer.parseInt(incrementCountString); + } + + private int getIndexFrom(HashMap arguments) + throws BadCommandException, NumberFormatException { + if (!arguments.containsKey(UpdateCommand.COMMAND_INDEX_ARGUMENT)) { + throw new BadCommandException(UpdateCommand.UPDATE_INVALID_ARGUMENTS_MESSAGE); + } + String indexString = arguments.get(UpdateCommand.COMMAND_INDEX_ARGUMENT); + return Integer.parseInt(indexString); + } + + private int getPositive(int changeCount) { + if (changeCount < 0) { + return -changeCount; + } else { + return changeCount; + } + } + + private boolean isPositive(int changeCount) { + return changeCount > 0; + } + + private void printFeedback(String updatedHabit, int changeCount) { + if (changeCount < 0) { + getTextUi().printOutputMessage(FEEDBACK_STRING_DECREMENT + LINE_SEPARATOR + + updatedHabit); + } else { + getTextUi().printOutputMessage(FEEDBACK_STRING_INCREMENT + LINE_SEPARATOR + + updatedHabit); + } + } + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return AtomicHabitManager.FEATURE_NAME; + } + + /** + * Executes the update command for atomic habits.
    + *

    + * This command is interactive, so user will continue providing arguments via + * further prompts provided. + */ + @Override + public void execute() throws AtomicHabitException { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + try { + if (getAtomicHabits().getAllHabits().isEmpty()) { + getTextUi().printOutputMessage(FEEDBACK_EMPTY_LIST_UPDATE); + return; + } + int changeCount = DEFAULT_INCREMENT; + boolean hasLevelUp = false; + if (super.getArguments().containsKey(UpdateCommand.COMMAND_INCREMENT_ARGUMENT)) { + changeCount = this.getIncrementCountFrom(super.getArguments()); + } + int index = this.getIndexFrom(super.getArguments()) - INDEX_OFFSET; + AtomicHabit habit = getAtomicHabits().getHabitByIndex(index); + if (changeCount == 0) { + getTextUi().printOutputMessage(FEEDBACK_CHANGE_COUNT_ZERO); + return; + } + if (changeCount > ZERO) { + habit.increaseCount(changeCount); + // Add XP for completing atomic habits as an incentive + hasLevelUp = gamificationData.addXp( + changeCount * NUM_OF_XP_PER_INCREMENT); + } else { + if (getPositive(changeCount) > habit.getCount()) { + throw new AtomicHabitException(FEEDBACK_DECREMENT_ERROR); + } + habit.decreaseCount(getPositive(changeCount)); + } + String stringOfUpdatedHabit = (index + 1) + DOT + habit + " " + "[" + habit.getCount() + "]" + + LINE_SEPARATOR; + printFeedback(stringOfUpdatedHabit, changeCount); + if (hasLevelUp) { + // Congratulate the user about levelling up + GamificationUi.printCelebrateLevelUp(); + } + } catch (NumberFormatException numberFormatException) { + LOGGER.log(Level.INFO, LOG_STR_INPUT_NOT_INTEGER); + throw new AtomicHabitException(FEEDBACK_INDEX_NOT_INTEGER_ERROR); + } catch (IndexOutOfBoundsException indexOutOfBoundsException) { + LOGGER.log(Level.INFO, LOG_INDEX_OUT_OF_BOUNDS); + throw new AtomicHabitException(FEEDBACK_INDEX_OUT_OF_BOUNDS_ERROR); + } catch (BadCommandException badCommandException) { + getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + } catch (StorageException storageException) { + getTextUi().printErrorFor(storageException, STORE_GAMIF_DATA_FAILED_NOTE_MESSAGE); + } + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser.
    + *

    + * If no exceptions are thrown, arguments are valid. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (!arguments.containsKey(UpdateCommand.COMMAND_KEYWORD)) { + throw new BadCommandException(UpdateCommand.COMMAND_INVALID_COMMAND_MESSAGE); + } + if (arguments.get(COMMAND_KEYWORD) != "") { + throw new BadCommandException(UpdateCommand.COMMAND_INVALID_PAYLOAD); + } + if (arguments.size() < UpdateCommand.COMMAND_MIN_NUM_OF_ARGUMENTS) { + throw new BadCommandException(UpdateCommand.COMMAND_INVALID_ARGUMENT_MESSAGE); + } + if (arguments.size() > UpdateCommand.COMMAND_MAX_NUM_OF_ARGUMENTS) { + throw new BadCommandException(UpdateCommand.COMMAND_INVALID_ARGUMENT_MESSAGE); + } + if (!arguments.containsKey(UpdateCommand.COMMAND_INDEX_ARGUMENT)) { + throw new BadCommandException(UpdateCommand.COMMAND_INVALID_ARGUMENT_MESSAGE); + } + if (arguments.size() == UpdateCommand.COMMAND_MAX_NUM_OF_ARGUMENTS + && !arguments.containsKey(UpdateCommand.COMMAND_INCREMENT_ARGUMENT)) { + throw new BadCommandException(UpdateCommand.COMMAND_INVALID_ARGUMENT_MESSAGE); + } + if (arguments.containsKey(UpdateCommand.COMMAND_INCREMENT_ARGUMENT)) { + String incrementString = arguments.get(COMMAND_INCREMENT_ARGUMENT); + if (incrementString.isBlank()) { + throw new BadCommandException(UpdateCommand.COMMAND_INVALID_PAYLOAD_INCREMENT); + } + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + +} diff --git a/src/main/java/wellnus/atomichabit/feature/AtomicHabit.java b/src/main/java/wellnus/atomichabit/feature/AtomicHabit.java new file mode 100644 index 0000000000..8c8d100581 --- /dev/null +++ b/src/main/java/wellnus/atomichabit/feature/AtomicHabit.java @@ -0,0 +1,56 @@ +package wellnus.atomichabit.feature; + +/** + * Class to represent a unique atomic habit that the user will practice + * It contains primarily the description of the habit and the count of the habit + */ +public class AtomicHabit { + private final String description; + private int count; + + /** + * Constructor of atomic habit class + * Will initialise private description to the input parameter + * Assigns count to 1 when a new habit is added + * + * @param description Description of this new atomic habit provided by the user + */ + public AtomicHabit(String description) { + this.description = description; + this.count = 0; + } + + /** + * Constructor of atomic habit class. + * Will initialise private description and count to the input parameter. + * + * @param description Description of atomic habit. + * @param count Number of habit to be initialized. + */ + public AtomicHabit(String description, int count) { + this.description = description; + this.count = count; + } + + public String getDescription() { + return description; + } + + public int getCount() { + return count; + } + + public void increaseCount(int increment) { + count += increment; + } + + public void decreaseCount(int decrement) { + count -= decrement; + } + + @Override + public String toString() { + return getDescription(); + } +} + diff --git a/src/main/java/wellnus/atomichabit/feature/AtomicHabitList.java b/src/main/java/wellnus/atomichabit/feature/AtomicHabitList.java new file mode 100644 index 0000000000..1e9de99fc0 --- /dev/null +++ b/src/main/java/wellnus/atomichabit/feature/AtomicHabitList.java @@ -0,0 +1,119 @@ +package wellnus.atomichabit.feature; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.common.WellNusLogger; +import wellnus.exception.StorageException; +import wellnus.exception.TokenizerException; +import wellnus.storage.AtomicHabitTokenizer; +import wellnus.storage.Storage; +import wellnus.ui.TextUi; + +/** + * Class to represent a container that will contain all unique AtomicHabit objects in an arraylist + */ +public class AtomicHabitList { + + private static final String TOKENIZER_ERROR = "Previous atomic habit data will not be restored."; + private static final String STORAGE_ERROR = "The data cannot be stored properly!!"; + private static final Logger LOGGER = WellNusLogger.getLogger("AtomicHabitListLogger"); + private static final AtomicHabitTokenizer atomicHabitTokenizer = new AtomicHabitTokenizer(); + private ArrayList allAtomicHabits; + + private Storage storage; + private TextUi textUi; + + /** + * Constructor for AtomicHabitList class, initializes the storage,textUi and allAtomicHabits objects. + * Loads the data from the data file into the arraylist of atomic habits. + */ + public AtomicHabitList() { + try { + this.storage = new Storage(); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + textUi.printErrorFor(storageException, STORAGE_ERROR); + } + textUi = new TextUi(); + allAtomicHabits = new ArrayList<>(); + try { + this.loadHabitData(); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + textUi.printErrorFor(storageException, STORAGE_ERROR); + } catch (TokenizerException tokenizerException) { + overrideErrorHabitData(); + LOGGER.log(Level.WARNING, TOKENIZER_ERROR); + textUi.printErrorFor(tokenizerException, TOKENIZER_ERROR); + } + } + + private void overrideErrorHabitData() { + ArrayList emptyTokenizedHabit = new ArrayList<>(); + try { + storage.saveData(emptyTokenizedHabit, Storage.FILE_HABIT); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + } + } + + /** + * Method to add atomicHabit to list containing all habits. + * + * @param atomicHabit New atomic habit to add into the list that this class manages + */ + + public void addAtomicHabit(AtomicHabit atomicHabit) { + allAtomicHabits.add(atomicHabit); + } + + /** + * Method to delete atomicHabit from the list containing all habits. + * + * @param atomicHabit Atomic habit to be deleted + */ + public void deleteAtomicHabit(AtomicHabit atomicHabit) { + allAtomicHabits.remove(atomicHabit); + } + + /** + * Tokenize the atomic habits and store them in a data file. + * + * @throws TokenizerException If there is error during tokenization + * @throws StorageException If data cannot be stored properly + */ + public void storeHabitData() throws StorageException { + ArrayList tokenizedHabitList = atomicHabitTokenizer.tokenize(allAtomicHabits); + storage.saveData(tokenizedHabitList, Storage.FILE_HABIT); + } + + /** + * Load a list of strings from data file and detokenize it into the values of atomic habits. + * + * @throws StorageException If there is error during tokenization + * @throws TokenizerException If there is error during detokenization + */ + public void loadHabitData() throws StorageException, TokenizerException { + boolean fileExists = storage.checkFileExists(Storage.FILE_HABIT); + ArrayList loadedHabitList = storage.loadData(Storage.FILE_HABIT); + if (fileExists) { + ArrayList detokenizedHabitList = atomicHabitTokenizer.detokenize(loadedHabitList); + allAtomicHabits = detokenizedHabitList; + } + } + + /** + * Method to get list containing all habits. + * + * @return allAtomicHabits which is an arraylist containing all habits + */ + public ArrayList getAllHabits() { + return allAtomicHabits; + } + + public AtomicHabit getHabitByIndex(int index) { + return allAtomicHabits.get(index); + } +} diff --git a/src/main/java/wellnus/atomichabit/feature/AtomicHabitManager.java b/src/main/java/wellnus/atomichabit/feature/AtomicHabitManager.java new file mode 100644 index 0000000000..c7e108e91a --- /dev/null +++ b/src/main/java/wellnus/atomichabit/feature/AtomicHabitManager.java @@ -0,0 +1,213 @@ +package wellnus.atomichabit.feature; + +import java.util.HashMap; + +import wellnus.atomichabit.command.AddCommand; +import wellnus.atomichabit.command.DeleteCommand; +import wellnus.atomichabit.command.HelpCommand; +import wellnus.atomichabit.command.HomeCommand; +import wellnus.atomichabit.command.ListCommand; +import wellnus.atomichabit.command.UpdateCommand; +import wellnus.command.Command; +import wellnus.exception.AtomicHabitException; +import wellnus.exception.BadCommandException; +import wellnus.exception.StorageException; +import wellnus.exception.WellNusException; +import wellnus.gamification.util.GamificationData; +import wellnus.manager.Manager; + + +/** + * Class to represent the event driver of Atomic Habits feature + * This class will handle calling the different available commands for Atomic Habits according to user input + */ +public class AtomicHabitManager extends Manager { + public static final String FEATURE_HELP_DESCRIPTION = "hb - Atomic Habits - Track and manage your habits " + + "with our suite of tools to help you grow and nurture a better you!"; + public static final String FEATURE_NAME = "hb"; + private static final String ADD_COMMAND_KEYWORD = "add"; + private static final String ATOMIC_HABIT_LOGO = " _ _ _ _ _ _ _ _ " + + System.lineSeparator() + + + " /_\\| |_ ___ _ __ (_)__ | || |__ _| |__(_) |_ ___" + System.lineSeparator() + + + " / _ \\ _/ _ \\ ' \\| / _| | __ / _` | '_ \\ | _(_-<" + System.lineSeparator() + + + " /_/ \\_\\__\\___/_|_|_|_\\__| |_||_\\__,_|_.__/_|\\__/__/" + System.lineSeparator(); + private static final String GREETING_MESSAGE = "Welcome to WellNUS++ Atomic Habits section!" + + System.lineSeparator() + "Track and inculcate good habits into your life with us!"; + private static final String HOME_COMMAND_KEYWORD = "home"; + private static final String LIST_COMMAND_KEYWORD = "list"; + private static final String UNKNOWN_COMMAND_MESSAGE = "Invalid command issued!"; + private static final String UPDATE_COMMAND_KEYWORD = "update"; + private static final String HELP_COMMAND_KEYWORD = "help"; + private static final String DELETE_COMMAND_KEYWORD = "delete"; + private static final String ERROR_STORAGE_MESSAGE = "Error saving to storage!"; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String COMMAND_INVALID_COMMAND_NOTE = + "Supported commands in Atomic Habit: " + LINE_SEPARATOR + + "add command " + AddCommand.COMMAND_USAGE + LINE_SEPARATOR + + "delete command " + DeleteCommand.COMMAND_USAGE + LINE_SEPARATOR + + "list command " + ListCommand.COMMAND_USAGE + LINE_SEPARATOR + + "update command " + UpdateCommand.COMMAND_USAGE + LINE_SEPARATOR + + "help command " + HelpCommand.COMMAND_USAGE + LINE_SEPARATOR + + "home command " + HomeCommand.COMMAND_USAGE; + private static final String ADD_USAGE = "add command " + AddCommand.COMMAND_USAGE; + private static final String DELETE_USAGE = "delete command " + DeleteCommand.COMMAND_USAGE; + private static final String HOME_USAGE = "home command " + HomeCommand.COMMAND_USAGE; + private static final String UPDATE_USAGE = "update command " + UpdateCommand.COMMAND_USAGE; + private final AtomicHabitUi atomicHabitUi; + private final AtomicHabitList habitList; + private final GamificationData gamificationData; + + /** + * Constructor of AtomicHabitManager + * Will initialise the private objects habitList and textUi + */ + public AtomicHabitManager(GamificationData gamificationData) { + this.gamificationData = gamificationData; + this.habitList = new AtomicHabitList(); + this.atomicHabitUi = new AtomicHabitUi(); + this.atomicHabitUi.setCursorName(FEATURE_NAME); + } + + /** + * Parses the given command from the user and determines the correct Command + * subclass that can handle its execution. + * + * @param commandString Full command issued by the user + * @return Command object that can execute the user's command + * @throws BadCommandException If an unknown command was issued by the user + */ + private Command getCommandFor(String commandString) throws BadCommandException { + HashMap arguments = getCommandParser().parseUserInput(commandString); + String commandKeyword = getCommandParser().getMainArgument(commandString); + switch (commandKeyword) { + case ADD_COMMAND_KEYWORD: + return new AddCommand(arguments, getHabitList()); + case DELETE_COMMAND_KEYWORD: + return new DeleteCommand(arguments, getHabitList()); + case HOME_COMMAND_KEYWORD: + return new HomeCommand(arguments); + case LIST_COMMAND_KEYWORD: + return new ListCommand(arguments, getHabitList()); + case UPDATE_COMMAND_KEYWORD: + return new UpdateCommand(arguments, getHabitList(), gamificationData); + case HELP_COMMAND_KEYWORD: + return new HelpCommand(arguments); + default: + throw new BadCommandException(UNKNOWN_COMMAND_MESSAGE); + } + } + + private AtomicHabitList getHabitList() { + return this.habitList; + } + + private AtomicHabitUi getTextUi() { + return this.atomicHabitUi; + } + + private void greet() { + getTextUi().printLogoWithSeparator(ATOMIC_HABIT_LOGO); + getTextUi().printOutputMessage(GREETING_MESSAGE); + } + + /** + * Reads user commands continuously and execute those that are supported + * until the exit command is given. + */ + private void runCommands() { + boolean isExit = false; + while (!isExit) { + try { + String commandString = getTextUi().getCommand(); + Command command = getCommandFor(commandString); + command.execute(); + try { + habitList.storeHabitData(); + } catch (StorageException exception) { + this.getTextUi().printErrorFor(exception, ERROR_STORAGE_MESSAGE); + } + isExit = HomeCommand.isExit(command); + } catch (WellNusException badCommandException) { + String errorMessage = badCommandException.getMessage(); + if (errorMessage.contains(ADD_COMMAND_KEYWORD)) { + getTextUi().printErrorFor(badCommandException, ADD_USAGE); + } else if (errorMessage.contains(DELETE_COMMAND_KEYWORD)) { + getTextUi().printErrorFor(badCommandException, DELETE_USAGE); + } else if (errorMessage.contains(HOME_COMMAND_KEYWORD)) { + getTextUi().printErrorFor(badCommandException, HOME_USAGE); + } else if (errorMessage.contains(UPDATE_COMMAND_KEYWORD)) { + getTextUi().printErrorFor(badCommandException, UPDATE_USAGE); + } else { + getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + } + } + } + } + + /** + * Returns the commandline name of the atomic habits feature + * + * @return Commandline name of this feature + */ + @Override + public String getFeatureName() { + return FEATURE_NAME; + } + + /** + * First welcomes user with our unique greeting.
    + *
    + * Then continuously read commands from the user and execute those that are supported. + */ + @Override + public void runEventDriver() { + greet(); + runCommands(); + } + + /** + * Method to test for exception handling of invalid command using JUnit + * + * @param userCommand Command identified after parsing through userInput + * @return Command according to userInput + * @throws AtomicHabitException For every invalid command being tested below + */ + public Command testInvalidCommand(String userCommand) throws AtomicHabitException { + String descriptionTest = "testing"; + String exitCommand = "hb exit"; + String listCommand = "hb list"; + String indexTest = "1"; + String invalidCommandErrorMessage = "Invalid command issued!!"; + HashMap arguments; + try { + switch (userCommand) { + case ADD_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(descriptionTest); + return new AddCommand(arguments, new AtomicHabitList()); + case DELETE_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(indexTest); + return new DeleteCommand(arguments, new AtomicHabitList()); + case LIST_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(listCommand); + return new ListCommand(arguments, new AtomicHabitList()); + case HOME_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(exitCommand); + return new HomeCommand(arguments); + case UPDATE_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(indexTest); + return new UpdateCommand(arguments, new AtomicHabitList(), gamificationData); + default: + throw new AtomicHabitException(invalidCommandErrorMessage); + } + } catch (BadCommandException badCommandException) { + getTextUi().printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return null; + } + } +} + + + diff --git a/src/main/java/wellnus/atomichabit/feature/AtomicHabitUi.java b/src/main/java/wellnus/atomichabit/feature/AtomicHabitUi.java new file mode 100644 index 0000000000..d46a048d8a --- /dev/null +++ b/src/main/java/wellnus/atomichabit/feature/AtomicHabitUi.java @@ -0,0 +1,40 @@ +package wellnus.atomichabit.feature; + +import java.io.InputStream; + +import wellnus.ui.TextUi; + +/** + * This class is to provide a customised interface and output message formatting for the atomic habit feature. + */ +public class AtomicHabitUi extends TextUi { + private static final String SEPARATOR = "~"; + + /** + * Call setSeparator() method inherited from TextUi superclass to re-define separator. + */ + public AtomicHabitUi() { + super(); + setSeparator(SEPARATOR); + } + + /** + * Constructor for AtomicHabitUi to include specified input stream for testing purposes. + * + * @param inputStream An InputStream object representing the input stream to be used + */ + public AtomicHabitUi(InputStream inputStream) { + super(inputStream); + setSeparator(SEPARATOR); + } + + private void printLogo(String logo) { + System.out.print(logo); + } + + protected void printLogoWithSeparator(String logo) { + printSeparator(); + printLogo(logo); + } +} + diff --git a/src/main/java/wellnus/command/Command.java b/src/main/java/wellnus/command/Command.java new file mode 100644 index 0000000000..4dc704abd8 --- /dev/null +++ b/src/main/java/wellnus/command/Command.java @@ -0,0 +1,131 @@ +package wellnus.command; + +import java.util.HashMap; +import java.util.Map; + +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; + +/** + * Superclass for all supported commands in Duke.
    + *

    + * Each Command is initialised with the arguments issued + * by the user. Execute the specified Command by calling + * execute().
    + *

    + * Child classes must provide the static isValidCommand() method for checking whether a set of + * arguments are valid for a given command. + */ +public abstract class Command { + private static final String ARGUMENT_DELIMITER = "--"; + private static final String DELIMITER_FOR_WORDS = " "; + private static final String WEIRD_ARGUMENTS_GIVEN = "Weird arguments given for command, cannot continue"; + // Key: An argument's name. Value: An argument's provided value from the user + private final HashMap arguments; + + /** + * Initialises a Command Object with the given arguments from the user + * + * @param arguments + */ + public Command(HashMap arguments) { + // Arguments should never be null, or later code will call methods on a null reference + assert arguments != null : WEIRD_ARGUMENTS_GIVEN; + this.arguments = arguments; + } + + protected HashMap getArguments() { + return this.arguments; + } + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + protected abstract String getCommandKeyword(); + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + protected abstract String getFeatureKeyword(); + + /** + * Executes the specified command from the user.
    + *

    + * May throw Exceptions if command fails. + * + * @throws WellNusException If command fails + */ + public abstract void execute() throws WellNusException; + + /** + * Very basic specialised toString() method for commands that returns + * a formatted list of all arguments issued by the user.
    + *

    + * Example: + * For the hb add command, toString() will output + * hb [add] [--name] <habit name> + * + * @return String Representation of this Command that includes all given arguments + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(this.getFeatureKeyword()); + builder.append(Command.DELIMITER_FOR_WORDS); + builder.append(this.getCommandKeyword()); + for (Map.Entry set : this.getArguments().entrySet()) { + builder.append(Command.DELIMITER_FOR_WORDS); + builder.append(Command.ARGUMENT_DELIMITER); + builder.append(set.getKey()); + builder.append(Command.DELIMITER_FOR_WORDS); + builder.append(set.getValue()); + } + return builder.toString(); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser.
    + *
    + * The validation logic and strictness is up to the implementer.
    + *
    + * As a guideline, isValidCommand should minimally:
    + *

  • Verify that ALL MANDATORY arguments exist
  • + *
  • Verify that ALL MANDATORY payloads exist
  • + *
  • Safely verify the payload type (int, date, etc should be properly processed)
  • + *
    + * Additionally, payload value cleanup (such as trimming) is also possible.
    + * As Java is pass (object reference) by value, any changes made to commandMap + * will persist out of the function call. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + public abstract void validateCommand(HashMap arguments) throws BadCommandException; + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + public abstract String getCommandUsage(); + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + public abstract String getCommandDescription(); + +} diff --git a/src/main/java/wellnus/command/CommandParser.java b/src/main/java/wellnus/command/CommandParser.java new file mode 100644 index 0000000000..895f908c8d --- /dev/null +++ b/src/main/java/wellnus/command/CommandParser.java @@ -0,0 +1,187 @@ +package wellnus.command; + +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; + +//@@author nichyjt + +/** + * A CommandParser processes user input from a defined format

    + *


    + * Each user input via console consists of:
    + *

      + *
    1. COMMANDS - A Argument and Payload pairs

    2. + *
    3. ARGUMENTS - String representing the action/parameters of the command

    4. + *
    5. PAYLOADS - value of the action/parameters

    6. + *
    + *

    + * In short, user input is a list of commands, each command containing arguments and payloads.

    + * Further, we define the FIRST command to be the MAIN command of any given user input.
    + * So, "deadline work on CS2113 --by Sunday" has "deadline work on CS2113" + * as the main command

    + * Each command (argument-payload pair) except for the main command MUST + * be delimited by " --" (whitespace intentional) + *

    + * For example, a given user input: "deadline work on CS2113 --by Sunday" + *

  • Has commands ["deadline work on CS2113", "by Sunday"]
  • + *
  • Has arguments ["deadline", "by"]
  • + *
  • Has payloads ["work on CS2113", ["Sunday"]
  • + *
    + */ +public class CommandParser { + + private static final String ARGUMENT_DELIMITER = " --"; + private static final String UNPADDED_DELIMITER = "--"; + private static final String PAYLOAD_DELIMITER = " "; + + // Message string constants for errors and ui + private static final String ERROR_EMPTY_COMMAND = "Invalid command issued, command cannot be empty!"; + private static final String ERROR_EMPTY_ARGUMENT = "Invalid arguments given, command is missing an argument!"; + private static final String ERROR_REPEATED_ARGUMENT = "Invalid arguments given, command cannot have " + + "repeated arguments!"; + private static final Logger LOGGER = WellNusLogger.getLogger("CommandParserLogger"); + private static final String LOG_STR_EMPTY_INPUT = "Input string is empty. This should be properly handled"; + private static final String LOG_EMPTY_ARG = "Argument is empty. This should be properly handled"; + + /** + * Constructs an instance of CommandParser.
    + *

    + * CommandParser should be used to break down raw user input into + * logical [Argument,Payload] pairs + */ + public CommandParser() { + + } + + /** + * Split a userInput by the standardized delimiter. + *

    + * This function handles some adversarial user input. + * There are 2 possible adversarial inputs that this function checks for: + *

    + * 1. Whitespace/Empty Arguments: `cmd payload -- payload1 -- `
    + * Split renders it as ["cmd payload", " payload1", ""] + * " payload1" will cause issues with rendering + * So, check for empty commands and whitespace prefix.
    + *

    + * 2. Missing main argument: `--argument payload`
    + * Split renders this as ["--argument payload"] + * So, check for "--" prefix. + * + * @param fullCommandString Raw user input from stdin in string form + * @return String array of command substrings + * @throws BadCommandException when command is empty or is problematic + */ + private String[] splitIntoCommands(String fullCommandString) throws BadCommandException { + assert fullCommandString != null : "fullCommandString should not be null"; + // Perform a string length sanity check + fullCommandString = fullCommandString.strip(); + if (fullCommandString.length() == 0) { + LOGGER.log(Level.INFO, LOG_STR_EMPTY_INPUT); + throw new BadCommandException(ERROR_EMPTY_COMMAND); + } + int noLimit = -1; + String[] rawCommands = fullCommandString.split(ARGUMENT_DELIMITER, noLimit); + String[] cleanCommands = new String[rawCommands.length]; + for (int i = 0; i < rawCommands.length; ++i) { + String currentCommand = rawCommands[i]; + // Case 1 check + if (currentCommand.startsWith(" ") || currentCommand.length() == 0) { + LOGGER.log(Level.INFO, LOG_EMPTY_ARG); + throw new BadCommandException(ERROR_EMPTY_ARGUMENT); + } + // Strip command of whitespace to clean input + currentCommand = currentCommand.strip(); + // Case 2 check + if (currentCommand.startsWith(UNPADDED_DELIMITER)) { + LOGGER.log(Level.INFO, LOG_EMPTY_ARG); + throw new BadCommandException(ERROR_EMPTY_COMMAND); + } + cleanCommands[i] = currentCommand; + } + return cleanCommands; + } + + private String getArgumentFromCommand(String commandString) throws BadCommandException { + assert commandString != null : "commandString should not be null"; + + String[] words = commandString.split(PAYLOAD_DELIMITER); + // Bad input checks + if (words.length == 0) { + LOGGER.log(Level.INFO, LOG_STR_EMPTY_INPUT); + throw new BadCommandException(ERROR_EMPTY_ARGUMENT); + } + return words[0].toLowerCase().strip(); + } + + private String getPayloadFromCommand(String commandString) { + assert commandString != null : "commandString should not be null"; + + String[] words = commandString.split(PAYLOAD_DELIMITER); + String payload = ""; + // Ignore the first word (Main Command), so start from 1 + for (int i = 1; i < words.length; ++i) { + payload = payload.concat(words[i]); + if (i != words.length - 1) { + payload = payload.concat(PAYLOAD_DELIMITER); + } + } + // No checks for payload length is done as payload CAN be empty + return payload.strip(); + } + + /** + * Takes in raw user input and splits it into Argument-Payload pairs + * + * @param userInput Raw user input from stdin in string form + * @return HashMap mapping a Argument (key) to a Payload (value) + * @throws BadCommandException when command is empty or is problematic + */ + public HashMap parseUserInput(String userInput) throws BadCommandException { + assert userInput != null : "userInput should not be null"; + + if (userInput.length() == 0) { + LOGGER.log(Level.INFO, LOG_STR_EMPTY_INPUT); + throw new BadCommandException(ERROR_EMPTY_COMMAND); + } + HashMap argumentPayload = new HashMap<>(); + String[] commands = splitIntoCommands(userInput); + for (String command : commands) { + String argument = getArgumentFromCommand(command); + // Safety check if arguments already exists + if (argumentPayload.containsKey(argument)) { + throw new BadCommandException(ERROR_REPEATED_ARGUMENT); + } + String payload = getPayloadFromCommand(command); + argumentPayload.put(argument, payload); + } + return argumentPayload; + } + + /** + * Takes in a string and returns the inferred "Main Argument" + *
    + * Practically, this is the First argument of any command string. + * For example, "hb add --name foobar"
    + * Has main argument "hb" + * + * @param userInput Any string input representing a command + * @return the inferred Main Argument, converted to lowercase + * @throws BadCommandException when String is empty + */ + public String getMainArgument(String userInput) throws BadCommandException { + assert userInput != null : "userInput should not be null"; + + userInput = userInput.strip(); + if (userInput.length() == 0) { + LOGGER.log(Level.INFO, LOG_STR_EMPTY_INPUT); + throw new BadCommandException(ERROR_EMPTY_COMMAND); + } + String[] parameters = userInput.split(" "); + return parameters[0].toLowerCase(); + } +} diff --git a/src/main/java/wellnus/command/ExitCommand.java b/src/main/java/wellnus/command/ExitCommand.java new file mode 100644 index 0000000000..a47910b2fc --- /dev/null +++ b/src/main/java/wellnus/command/ExitCommand.java @@ -0,0 +1,113 @@ +package wellnus.command; + +import java.util.HashMap; + +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; +import wellnus.ui.TextUi; + +/** + * Provides the exit command of the WellNUS++ app. + */ +public class ExitCommand extends Command { + public static final String COMMAND_DESCRIPTION = "exit - Close WellNUS++ and return to your terminal."; + public static final String COMMAND_USAGE = "usage: exit"; + public static final String COMMAND_KEYWORD = "exit"; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'exit'!"; + private static final String EXTRA_PAYLOAD_MESSAGE = "Invalid payload given to 'exit'!"; + private static final String FEATURE_KEYWORD = ""; + private static final int NUM_OF_ARGUMENTS = 1; + private static final String INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'exit'"; + private final TextUi textUi; + + /** + * Initialises an ExitCommand Object using the arguments issued by the user. + * + * @param arguments Command arguments issued by the user + * @see ExitCommand#validateCommand(HashMap) + */ + public ExitCommand(HashMap arguments) { + super(arguments); + this.textUi = new TextUi(); + } + + public static boolean isExit(Command command) { + return command instanceof ExitCommand; + } + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return ExitCommand.COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return ExitCommand.FEATURE_KEYWORD; + } + + /** + * Exits the WellNUS++ application. + */ + @Override + public void execute() throws WellNusException { + validateCommand(super.getArguments()); + } + + /** + * Validate the arguments passed by the user + * + * @param arguments Argument-Payload map generated by CommandParser using the user's command + * @throws BadCommandException If the commandMap has any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (!arguments.containsKey(ExitCommand.COMMAND_KEYWORD)) { + throw new BadCommandException(ExitCommand.COMMAND_INVALID_COMMAND_MESSAGE); + } + if (arguments.size() > NUM_OF_ARGUMENTS) { + throw new BadCommandException(ExitCommand.INVALID_ARGUMENTS_MESSAGE); + } + String commandPayload = arguments.get(ExitCommand.COMMAND_KEYWORD); + if (!commandPayload.isBlank()) { + throw new BadCommandException(EXTRA_PAYLOAD_MESSAGE); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/command/HelpCommand.java b/src/main/java/wellnus/command/HelpCommand.java new file mode 100644 index 0000000000..d03d377261 --- /dev/null +++ b/src/main/java/wellnus/command/HelpCommand.java @@ -0,0 +1,204 @@ +package wellnus.command; + +import java.util.ArrayList; +import java.util.HashMap; + +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.common.MainManager; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.gamification.GamificationManager; +import wellnus.reflection.feature.ReflectionManager; +import wellnus.ui.TextUi; + +//@@author nichyjt + +/** + * Implementation of WellNus' help command. Explains to the user what commands are supported + * by WellNus and how to use each command. + */ +public class HelpCommand extends Command { + public static final String COMMAND_DESCRIPTION = "help - Get help on what commands can be used in WellNUS++."; + public static final String COMMAND_USAGE = "usage: help [command-to-check]"; + private static final String COMMAND_KEYWORD = "help"; + private static final String BAD_ARGUMENTS_MESSAGE = "Invalid arguments given to 'help'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'help'!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "help command " + COMMAND_USAGE; + private static final String NO_FEATURE_KEYWORD = ""; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String HELP_PREAMBLE = "Input `help` to see all available commands." + LINE_SEPARATOR + + "Input `help [command-to-check]` to get usage help for a specific command." + LINE_SEPARATOR + + "Here are all the commands available for you!"; + private static final String USAGE_HABIT = "\tusage: hb"; + private static final String USAGE_REFLECT = "\tusage: reflect"; + private static final String USAGE_FOCUS = "\tusage: ft"; + private static final String USAGE_GAMIFICATION = "\tusage: gamif"; + private static final String PADDING = " "; + private static final String DOT = "."; + private static final int ONE_OFFSET = 1; + private static final int EXPECTED_PAYLOAD_SIZE = 1; + private final TextUi textUi; + + /** + * Initialises a HelpCommand Object using the command arguments issued by the user. + * + * @param arguments Command arguments issued by the user + */ + public HelpCommand(HashMap arguments) { + super(arguments); + this.textUi = new TextUi(); + } + + private TextUi getTextUi() { + return this.textUi; + } + + private ArrayList getCommandDescriptions() { + ArrayList commandDescriptions = new ArrayList<>(); + commandDescriptions.add(AtomicHabitManager.FEATURE_HELP_DESCRIPTION); + commandDescriptions.add(ReflectionManager.FEATURE_HELP_DESCRIPTION); + commandDescriptions.add(FocusManager.FEATURE_HELP_DESCRIPTION); + commandDescriptions.add(GamificationManager.FEATURE_HELP_DESCRIPTION); + commandDescriptions.add(ExitCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(COMMAND_DESCRIPTION); + return commandDescriptions; + } + + /** + * Prints either the general help message or the command-specific help message + * based on the presence of a payload. + */ + private void printHelpMessage() { + HashMap argumentPayload = getArguments(); + String commandToSearch = argumentPayload.get(COMMAND_KEYWORD).trim().toLowerCase(); + if (commandToSearch.equals(NO_FEATURE_KEYWORD)) { + printGeneralHelpMessage(); + return; + } + printSpecificHelpMessage(commandToSearch); + } + + /** + * Lists all features available in WellNUS++ and a short description. + */ + public void printGeneralHelpMessage() { + ArrayList commandDescriptions = getCommandDescriptions(); + String outputMessage = MainManager.FEATURE_HELP_DESCRIPTION; + outputMessage = outputMessage.concat(System.lineSeparator()); + outputMessage = outputMessage.concat(HELP_PREAMBLE); + outputMessage = outputMessage.concat(System.lineSeparator() + System.lineSeparator()); + + for (int i = 0; i < commandDescriptions.size(); i += 1) { + outputMessage = outputMessage.concat(i + ONE_OFFSET + DOT + PADDING); + outputMessage = outputMessage.concat(commandDescriptions.get(i) + System.lineSeparator()); + } + this.getTextUi().printOutputMessage(outputMessage); + } + + /** + * Prints the help message for a given commandToSearch.
    + * If the commandToSearch does not exist, help will print an unknown command + * error message. + */ + public void printSpecificHelpMessage(String commandToSearch) { + switch (commandToSearch) { + case AtomicHabitManager.FEATURE_NAME: + printUsageMessage(AtomicHabitManager.FEATURE_HELP_DESCRIPTION, USAGE_HABIT); + break; + case ReflectionManager.FEATURE_NAME: + printUsageMessage(ReflectionManager.FEATURE_HELP_DESCRIPTION, USAGE_REFLECT); + break; + case FocusManager.FEATURE_NAME: + printUsageMessage(FocusManager.FEATURE_HELP_DESCRIPTION, USAGE_FOCUS); + break; + case GamificationManager.FEATURE_NAME: + printUsageMessage(GamificationManager.FEATURE_HELP_DESCRIPTION, USAGE_GAMIFICATION); + break; + case HelpCommand.COMMAND_KEYWORD: + printUsageMessage(HelpCommand.COMMAND_DESCRIPTION, HelpCommand.COMMAND_USAGE); + break; + case ExitCommand.COMMAND_KEYWORD: + printUsageMessage(ExitCommand.COMMAND_DESCRIPTION, ExitCommand.COMMAND_USAGE); + break; + default: + BadCommandException unknownCommand = new BadCommandException(COMMAND_INVALID_PAYLOAD); + textUi.printErrorFor(unknownCommand, COMMAND_INVALID_COMMAND_NOTE); + } + } + + private void printUsageMessage(String commandDescription, String usageString) { + String message = commandDescription + System.lineSeparator() + usageString; + textUi.printOutputMessage(message); + } + + @Override + protected String getCommandKeyword() { + return HelpCommand.COMMAND_KEYWORD; + } + + @Override + protected String getFeatureKeyword() { + return HelpCommand.NO_FEATURE_KEYWORD; + } + + /** + * Executes the issued help command.
    + *

    + * Prints a brief description of all of WellNus' supported commands if + * the basic 'help' command was issued.
    + *

    + * Prints a detailed description of a specific feature if the specialised + * 'help' command was issued. + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException exception) { + getTextUi().printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + return; + } + this.printHelpMessage(); + } + + /** + * Checks whether the given arguments are valid for our help command. + * + * @param arguments Argument-Payload map generated by CommandParser using user's command + * @throws BadCommandException If the command is invalid + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(COMMAND_KEYWORD) : "HelpCommand's payload map does not contain 'help'!"; + // Check if user put in unnecessary payload or arguments + if (arguments.size() > EXPECTED_PAYLOAD_SIZE) { + throw new BadCommandException(BAD_ARGUMENTS_MESSAGE); + } + } + + /** + * Abstract method to ensure developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/common/MainManager.java b/src/main/java/wellnus/common/MainManager.java new file mode 100644 index 0000000000..6fccf9244b --- /dev/null +++ b/src/main/java/wellnus/common/MainManager.java @@ -0,0 +1,236 @@ +package wellnus.common; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.command.Command; +import wellnus.command.CommandParser; +import wellnus.command.ExitCommand; +import wellnus.command.HelpCommand; +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; +import wellnus.focus.feature.FocusManager; +import wellnus.gamification.GamificationManager; +import wellnus.manager.Manager; +import wellnus.reflection.feature.ReflectionManager; +import wellnus.ui.TextUi; + +/** + * MainManager is the primary event driver for WellNUS++
    + *
    + * MainManager creates and stores exactly one instance of each feature's Manager in WellNUS++. + *

    + * It runs an event driver, matches user input to the selected feature + * and executes its instance to launch the feature Manager. + */ +public class MainManager extends Manager { + public static final String FEATURE_HELP_DESCRIPTION = "WellNUS++ is a Command Line Interface (CLI)" + + " app for you to keep track, manage and improve your physical and mental wellness."; + protected static final String EXIT_COMMAND_KEYWORD = "exit"; + protected static final String HELP_COMMAND_KEYWORD = "help"; + private static final String COMMAND_IS_BLANK_MESSAGE = "Command is blank - please check user input code for " + + "MainManager."; + private static final String COMMAND_IS_NULL_MESSAGE = "Command is null - please check user input code for " + + "MainManager."; + private static final String FEATURE_NAME = "main"; + private static final String GREETING_MESSAGE = "Enter a command to start using WellNUS++! Try 'help' " + + "if you're new, or just unsure."; + private static final String INVALID_COMMAND_MESSAGE = "Invalid command issued!"; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String INVALID_COMMAND_ADDITIONAL_MESSAGE = + "Supported features: " + LINE_SEPARATOR + + "Access Atomic Habit: hb" + LINE_SEPARATOR + + "Access Self Reflection: reflect" + LINE_SEPARATOR + + "Access Focus Timer: ft" + LINE_SEPARATOR + + "Access Gamification: gamif" + LINE_SEPARATOR + + "Help command: help" + LINE_SEPARATOR + + "Exit program: exit"; + private static final String INVALID_FEATURE_KEYWORD_MESSAGE = "Feature keyword can't be empty dear"; + private static final int NUM_OF_ARGUMENTS = 1; + private static final String INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to '%s'!"; + private static final String UNNECESSARY_PAYLOAD_MESSAGE = "Invalid payload given to '%s', drop the '%s' " + + "and try again!"; + private static final String WELLNUS_FEATURE_NAME = ""; + private final ArrayList featureManagers; + private boolean hasExecutedCommands = false; + private final TextUi textUi; + + /** + * Constructs an instance of MainManager.
    + * Instantiates boilerplate utilities like TextUi + * and populates featureManagers with exactly one instance to be executed on user selection. + */ + public MainManager() { + super(); + this.featureManagers = new ArrayList<>(); + this.textUi = new TextUi(); + this.textUi.setCursorName(FEATURE_NAME); + } + + /** + * Parses the given command String issued by the user and returns the corresponding + * Command object that can execute it. + * + * @param command Command issued by the user + * @return Command object that can execute the user's command + * @throws BadCommandException If command issued is not supported or invalid + */ + protected Command getMainCommandFor(String command) throws BadCommandException { + String commandKeyword = getCommandParser().getMainArgument(command); + HashMap arguments = getCommandParser().parseUserInput(command); + switch (commandKeyword) { + case MainManager.HELP_COMMAND_KEYWORD: + return new HelpCommand(arguments); + case MainManager.EXIT_COMMAND_KEYWORD: + return new ExitCommand(arguments); + default: + throw new BadCommandException(MainManager.INVALID_COMMAND_MESSAGE); + } + } + + protected Optional getManagerFor(String featureKeyword) { + assert (featureKeyword != null && !featureKeyword.isBlank()) + : MainManager.INVALID_FEATURE_KEYWORD_MESSAGE; + for (Manager featureManager : this.getSupportedFeatureManagers()) { + if (featureManager.getFeatureName().equals(featureKeyword)) { + return Optional.of(featureManager); + } + } + return Optional.empty(); + } + + /** + * Continuously reads user's commands and executes those that are supported + * by WellNUS++ until the `exit` command is given.
    + *

    + * If an unrecognised command is given, a warning is printed on the user's screen. + */ + private void executeCommands() { + if (!hasExecutedCommands) { + this.setSupportedFeatureManagers(); + hasExecutedCommands = true; + } + boolean isExit = false; + CommandParser parser = new CommandParser(); + while (!isExit) { + try { + String nextCommand = this.getTextUi().getCommand(); + validate(nextCommand); + // nextCommand now guaranteed to be a supported feature/main command + String featureKeyword = parser.getMainArgument(nextCommand); + Optional featureManager = this.getManagerFor(featureKeyword); + // User issued a feature keyword, pass control to the corresponding feature's Manager + featureManager.ifPresent((manager) -> { + try { + manager.runEventDriver(); + } catch (BadCommandException badCommandException) { + this.getTextUi().printErrorFor(badCommandException, INVALID_COMMAND_ADDITIONAL_MESSAGE); + } + }); + // User issued a main command, e.g. 'help' + if (featureManager.isEmpty()) { + Command mainCommand = this.getMainCommandFor(nextCommand); + mainCommand.execute(); + isExit = ExitCommand.isExit(mainCommand); + } + } catch (BadCommandException badCommandException) { + this.getTextUi().printErrorFor(badCommandException, MainManager.INVALID_COMMAND_ADDITIONAL_MESSAGE); + } catch (WellNusException exception) { + this.getTextUi().printErrorFor(exception, INVALID_COMMAND_ADDITIONAL_MESSAGE); + } + } + // We are about to quit WellNUS++. Close the log file used in this session + WellNusLogger.closeLogFile(); + } + + private List getSupportedCommandKeywords() { + List commandKeywords = new ArrayList<>(); + commandKeywords.add(MainManager.HELP_COMMAND_KEYWORD); + commandKeywords.add(MainManager.EXIT_COMMAND_KEYWORD); + return commandKeywords; + } + + private List getSupportedFeatureManagers() { + return this.featureManagers; + } + + private TextUi getTextUi() { + return this.textUi; + } + + private void greet() { + this.getTextUi().printOutputMessage(MainManager.GREETING_MESSAGE); + } + + private boolean isSupportedCommand(String commandKeyword) { + List cmdKeywords = this.getSupportedCommandKeywords(); + for (String cmdKeyword : cmdKeywords) { + if (commandKeyword.equals(cmdKeyword)) { + return true; + } + } + return false; + } + + private void validate(String command) throws BadCommandException { + assert command != null : MainManager.COMMAND_IS_NULL_MESSAGE; + assert !command.isBlank() : MainManager.COMMAND_IS_BLANK_MESSAGE; + String featureKeyword = commandParser.getMainArgument(command); + Optional featureManager = this.getManagerFor(featureKeyword); + // User gave a command that's not any feature's keyword nor a recognised main command + if (featureManager.isEmpty() && !this.isSupportedCommand(featureKeyword)) { + throw new BadCommandException(MainManager.INVALID_COMMAND_MESSAGE); + } + HashMap arguments = commandParser.parseUserInput(command); + if (arguments.size() > NUM_OF_ARGUMENTS) { + throw new BadCommandException(String.format(MainManager.INVALID_ARGUMENTS_MESSAGE, + featureKeyword)); + } + String argumentPayload = arguments.get(featureKeyword); + if (!featureKeyword.equals(HELP_COMMAND_KEYWORD) && !argumentPayload.isBlank()) { + throw new BadCommandException(String.format(MainManager.UNNECESSARY_PAYLOAD_MESSAGE, + featureKeyword, argumentPayload)); + } + } + + /** + * Returns the name of this feature. In this case, it's just empty(not any particular feature). + * + * @return Empty String to imply that this Manager is not associated with any feature + */ + @Override + public String getFeatureName() { + return WELLNUS_FEATURE_NAME; + } + + /** + * Executes the basic commands(e.g. help) as well as any feature-specific + * commands, which are delegated to the corresponding features' Managers.
    + *
    + * This method will keep reading the user's command until the exit command is given. + */ + @Override + public void runEventDriver() { + this.greet(); + this.executeCommands(); + } + + /** + * Returns a list of features supported by WellNUS++.
    + *
    + * Suggested implementation:
    + * this.supportedManagers.add([mgr1, mgr2, ...]); + */ + protected void setSupportedFeatureManagers() { + GamificationManager gamificationManager = new GamificationManager(); + this.getSupportedFeatureManagers().add(gamificationManager); + this.getSupportedFeatureManagers().add( + new AtomicHabitManager(gamificationManager.getGamificationData())); + this.getSupportedFeatureManagers().add(new ReflectionManager()); + this.getSupportedFeatureManagers().add(new FocusManager()); + } + +} diff --git a/src/main/java/wellnus/common/WellNusLogger.java b/src/main/java/wellnus/common/WellNusLogger.java new file mode 100644 index 0000000000..c857ab0a27 --- /dev/null +++ b/src/main/java/wellnus/common/WellNusLogger.java @@ -0,0 +1,131 @@ +package wellnus.common; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.FileHandler; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +import wellnus.exception.StorageException; +import wellnus.ui.TextUi; + +/** + * Wrapper class for java.util.logging.Logger that redirects logging + * to a specific log file instead of printing it on the user's screen. + * @see Logger + */ +public class WellNusLogger { + private static final String CREATE_LOG_FILE_IO_EXCEPTION_MESSAGE = "Failed to create log file."; + private static final String EXCEPTION_NOTE_MESSAGE = "Logging will not be saved to a log file during this app " + + "session."; + private static final int FIVE_MEGABYTES = 5 * 1024 * 1024; + private static final String INVALID_LOG_PATH_MESSAGE = "Invalid log path, cannot create log file."; + private static final boolean IS_LOG_FILE_APPEND_MODE = true; + private static final String LOG_DIR_PATH = "log/"; + private static final String LOG_FILE_NAME = "wellnus.log"; + private static final String LOG_PATH_BLANK_MESSAGE = "Blank log path passed to WellNusLogger.checkLogPath()."; + private static final String LOG_PATH_NULL_MESSAGE = "null log path passed to WellNusLogger.checkLogPath()."; + private static final String LOGGER_NAME_BLANK_MESSAGE = "Name parameter given to WellNusLogger.getLogger() cannot " + + "be blank"; + private static final String LOGGER_NAME_NULL_MESSAGE = "Name parameter given to WellNusLogger.getLogger() cannot " + + "be null"; + private static final String IO_EXCEPTION_MESSAGE = "Failed to load log file."; + private static final int MAX_LOG_FILE_SIZE_MEGA_BYTES = 5; + private static final int MEGABYTE_DIVISOR = 1024 * 1024; + private static final int NUM_LOG_FILE = 1; + private static final String SECURITY_EXCEPTION_MESSAGE = "Unable to create log file due to security policies."; + private static final String UNKNOWN_ERROR_MESSAGE = "Unable to create log file due to unknown error."; + private static Optional logFileHandler = Optional.empty(); + + private static void checkLogPath(String logPath) { + assert logPath != null : LOG_PATH_NULL_MESSAGE; + assert !logPath.isBlank() : LOG_PATH_BLANK_MESSAGE; + TextUi textUi = new TextUi(); + try { + File logFile = new File(logPath); + Path parentDir = logFile.getParentFile().toPath(); + Files.createDirectories(parentDir); + if (!logFile.exists()) { + logFile.createNewFile(); + } else { + long logFileSizeInMegaBytes = logFile.length() / MEGABYTE_DIVISOR; + if (logFileSizeInMegaBytes <= MAX_LOG_FILE_SIZE_MEGA_BYTES) { + return; + } + // Log file is more than 5 MBs in size, clear it to preserve user's storage space + logFile.delete(); + logFile.createNewFile(); + } + } catch (InvalidPathException invalidPathException) { + StorageException storageException = new StorageException(INVALID_LOG_PATH_MESSAGE); + textUi.printErrorFor(storageException, EXCEPTION_NOTE_MESSAGE); + } catch (IOException ioException) { + StorageException storageException = new StorageException(CREATE_LOG_FILE_IO_EXCEPTION_MESSAGE); + textUi.printErrorFor(storageException, EXCEPTION_NOTE_MESSAGE); + } catch (SecurityException securityException) { + StorageException storageException = new StorageException(SECURITY_EXCEPTION_MESSAGE); + textUi.printErrorFor(storageException, EXCEPTION_NOTE_MESSAGE); + } + } + + private static FileHandler getFileHandler() { + if (logFileHandler.isPresent()) { + return logFileHandler.get(); + } + FileHandler fileHandler = null; + TextUi textUi = new TextUi(); + try { + String logPath = LOG_DIR_PATH + LOG_FILE_NAME; + checkLogPath(logPath); + fileHandler = new FileHandler(logPath, FIVE_MEGABYTES, NUM_LOG_FILE, IS_LOG_FILE_APPEND_MODE); + SimpleFormatter simpleFormatter = new SimpleFormatter(); + fileHandler.setFormatter(simpleFormatter); + logFileHandler = Optional.of(fileHandler); + } catch (SecurityException securityException) { + StorageException storageException = new StorageException(SECURITY_EXCEPTION_MESSAGE); + textUi.printErrorFor(storageException, EXCEPTION_NOTE_MESSAGE); + } catch (IOException ioException) { + StorageException storageException = new StorageException(IO_EXCEPTION_MESSAGE); + textUi.printErrorFor(storageException, EXCEPTION_NOTE_MESSAGE); + } + return fileHandler; + } + + /** + * Closes the log file used by WellNUS++. + */ + public static void closeLogFile() { + logFileHandler.ifPresent(FileHandler::close); + } + + /** + * Returns an instance of Java's Logger class that directs all logging to a specific + * log file instead of the user's screen. + * @param loggerName Name of the logger instance to create and return + * @return Instance of Logger that logs to a specific file + */ + public static Logger getLogger(String loggerName) { + assert loggerName != null : LOGGER_NAME_NULL_MESSAGE; + assert !loggerName.isBlank() : LOGGER_NAME_BLANK_MESSAGE; + Logger logger = Logger.getLogger(loggerName); + TextUi textUi = new TextUi(); + FileHandler fileHandler = getFileHandler(); + if (fileHandler != null) { + try { + logger.addHandler(fileHandler); + logger.setUseParentHandlers(false); + } catch (SecurityException securityException) { + StorageException storageException = new StorageException(SECURITY_EXCEPTION_MESSAGE); + textUi.printErrorFor(storageException, EXCEPTION_NOTE_MESSAGE); + } + } else { + StorageException storageException = new StorageException(UNKNOWN_ERROR_MESSAGE); + textUi.printErrorFor(storageException, EXCEPTION_NOTE_MESSAGE); + } + return logger; + } +} diff --git a/src/main/java/wellnus/exception/AtomicHabitException.java b/src/main/java/wellnus/exception/AtomicHabitException.java new file mode 100644 index 0000000000..fc199b934b --- /dev/null +++ b/src/main/java/wellnus/exception/AtomicHabitException.java @@ -0,0 +1,14 @@ +package wellnus.exception; + +/** + * AtomicHabitException is thrown when a conceptual/logical error occurs in the AtomicHabit package
    + *

    + * This exception should only be used within the AtomicHabit package. + * It differentiates between WellNUS and regular Java exceptions, + * which allows better pinpointing of errors to the AtomicHabit logic. + */ +public class AtomicHabitException extends WellNusException { + public AtomicHabitException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/wellnus/exception/BadCommandException.java b/src/main/java/wellnus/exception/BadCommandException.java new file mode 100644 index 0000000000..be574061c7 --- /dev/null +++ b/src/main/java/wellnus/exception/BadCommandException.java @@ -0,0 +1,14 @@ +package wellnus.exception; + +/** + * BadCommandException is thrown when a conceptual/logical error occurs in Command (sub)classes
    + *

    + * This exception should be used in classes extending from the Command class. + * It differentiates between WellNUS and regular Java exceptions, + * allowing better pinpointing of errors to Command subclasses. + */ +public class BadCommandException extends WellNusException { + public BadCommandException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/wellnus/exception/FocusException.java b/src/main/java/wellnus/exception/FocusException.java new file mode 100644 index 0000000000..b894589349 --- /dev/null +++ b/src/main/java/wellnus/exception/FocusException.java @@ -0,0 +1,13 @@ +package wellnus.exception; + +/** + * FocusException is thrown when a conceptual/logical error occurs in Focus. + *

    + * This exception is only thrown within the functional code for Focus. + */ +public class FocusException extends WellNusException { + public FocusException(String message) { + super(message); + } + +} diff --git a/src/main/java/wellnus/exception/GamificationException.java b/src/main/java/wellnus/exception/GamificationException.java new file mode 100644 index 0000000000..a410bda0cb --- /dev/null +++ b/src/main/java/wellnus/exception/GamificationException.java @@ -0,0 +1,14 @@ +package wellnus.exception; + +/** + * Category of Exceptions related to the gamification feature. + */ +public class GamificationException extends WellNusException { + /** + * Returns an instance of the GamificationException. + * @param message Error message to display on the user's screen + */ + public GamificationException(String message) { + super(message); + } +} diff --git a/src/main/java/wellnus/exception/ReflectionException.java b/src/main/java/wellnus/exception/ReflectionException.java new file mode 100644 index 0000000000..c5138c3a6b --- /dev/null +++ b/src/main/java/wellnus/exception/ReflectionException.java @@ -0,0 +1,12 @@ +package wellnus.exception; + +/** + * ReflectionException is thrown when a conceptual/logical error occurs in the reflection package
    + *

    + * This exception should only be used within the reflection package. + */ +public class ReflectionException extends WellNusException { + public ReflectionException(String message) { + super(message); + } +} diff --git a/src/main/java/wellnus/exception/StorageException.java b/src/main/java/wellnus/exception/StorageException.java new file mode 100644 index 0000000000..a77e9e31bb --- /dev/null +++ b/src/main/java/wellnus/exception/StorageException.java @@ -0,0 +1,12 @@ +package wellnus.exception; + +/** + * StorageException is thrown when a conceptual/logical error occurs in Storage. + *

    + * This exception is only thrown within the functional code for Storage. + */ +public class StorageException extends Exception { + public StorageException(String message) { + super(message); + } +} diff --git a/src/main/java/wellnus/exception/TokenizerException.java b/src/main/java/wellnus/exception/TokenizerException.java new file mode 100644 index 0000000000..07a38da65b --- /dev/null +++ b/src/main/java/wellnus/exception/TokenizerException.java @@ -0,0 +1,15 @@ +package wellnus.exception; + +/** + * Category of Exceptions related to the Tokenizer interface and its operations/subclasses. + * @see wellnus.storage.Tokenizer + */ +public class TokenizerException extends WellNusException { + /** + * Initializes an instance of TokenizerException with the given error message. + * @param errorMessage Error message to show to the user + */ + public TokenizerException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/wellnus/exception/WellNusException.java b/src/main/java/wellnus/exception/WellNusException.java new file mode 100644 index 0000000000..3bcd536dc0 --- /dev/null +++ b/src/main/java/wellnus/exception/WellNusException.java @@ -0,0 +1,13 @@ +package wellnus.exception; + +/** + * WellNusException is thrown when a conceptual/logical error occurs in WellNUS++ + *

    + * This exception may be thrown for any general error in WellNUS++'s execution. + * It is meant to differentiate between Java exceptions to allow better pinpointing of errors. + */ +public class WellNusException extends Exception { + public WellNusException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/wellnus/focus/command/CheckCommand.java b/src/main/java/wellnus/focus/command/CheckCommand.java new file mode 100644 index 0000000000..7f0c8953ab --- /dev/null +++ b/src/main/java/wellnus/focus/command/CheckCommand.java @@ -0,0 +1,135 @@ +package wellnus.focus.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +/** + * Represents a command to check the time left in the current session. + */ +public class CheckCommand extends Command { + + public static final String COMMAND_DESCRIPTION = "check - Check the time left in the current countdown." + + "Only usable when a countdown is not finished!"; + public static final String COMMAND_USAGE = "usage: check"; + public static final String COMMAND_KEYWORD = "check"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'check'"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'check'!"; + private static final String CHECK_OUTPUT = "Time left: "; + private static final String ERROR_COUNTDOWN_NOT_RUNNING = "Nothing to check - the countdown has not started yet!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "check command " + COMMAND_USAGE; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'check'!"; + private final Session session; + private final FocusUi focusUi; + + /** + * Constructs a CheckCommand object. + * Session in FocusManager is passed into this class. + * + * @param arguments Argument-Payload Hashmap generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public CheckCommand(HashMap arguments, Session session) { + super(arguments); + this.session = session; + this.focusUi = new FocusUi(); + } + + /** + * Identifies this Command's keyword. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Checks the current time left in the current countdown. + * Prints the time left in the current countdown. + */ + @Override + public void execute() { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + focusUi.printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + if (session.isSessionReady()) { + focusUi.printOutputMessage(ERROR_COUNTDOWN_NOT_RUNNING); + return; + } + if (session.isSessionWaiting()) { + focusUi.printOutputMessage(ERROR_COUNTDOWN_NOT_RUNNING); + return; + } + int minutes = session.getCurrentCountdown().getMinutes(); + int seconds = session.getCurrentCountdown().getSeconds(); + focusUi.printOutputMessage(CHECK_OUTPUT + String.format("%d:%d", minutes, seconds)); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser. + * User input is valid if no exceptions are thrown. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(COMMAND_INVALID_COMMAND_MESSAGE); + } + if (!arguments.get(COMMAND_KEYWORD).equals("")) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/focus/command/ConfigCommand.java b/src/main/java/wellnus/focus/command/ConfigCommand.java new file mode 100644 index 0000000000..e42cf2e08e --- /dev/null +++ b/src/main/java/wellnus/focus/command/ConfigCommand.java @@ -0,0 +1,361 @@ +package wellnus.focus.command; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.exception.FocusException; +import wellnus.exception.WellNusException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +//@@author nichyjt + +/** + * ConfigCommand sets the configuration for a Session's parameters. + *

    + * These parameters are: cycles, work time, break time, long break time. + */ +public class ConfigCommand extends Command { + public static final String COMMAND_DESCRIPTION = "config - Change the number of cycles " + + "and length of your work, break and longbreak timings!"; + public static final String COMMAND_USAGE = "usage: config [--cycle number] [--work minutes] " + + "[--break minutes] [--longbreak minutes]"; + protected static final String COMMAND_KEYWORD = "config"; + protected static final String ARGUMENT_CYCLE = "cycle"; + protected static final String ARGUMENT_WORK = "work"; + protected static final String ARGUMENT_BREAK = "break"; + protected static final String ARGUMENT_LONG_BREAK = "longbreak"; + private static final String PRINT_CONFIG_MESSAGE = "Okay, here's your configured session details!" + + System.lineSeparator(); + private static final String PRINT_CONFIG_CYCLES = "Cycles: "; + private static final String PRINT_CONFIG_WORK = "Work: "; + private static final String PRINT_CONFIG_BREAK = "Break: "; + private static final String PRINT_CONFIG_LONG_BREAK = "Long break: "; + private static final String SINGLE_SPACE_PAD = " "; + private static final String PRINT_CONFIG_MINS = "minutes"; + private static final String PRINT_CONFIG_MIN = "minute"; + private static final String EMPTY_STRING = ""; + private static final int COMMAND_MAX_NUM_ARGUMENTS = 5; + private static final int COMMAND_MIN_NUM_ARGUMENTS = 1; + private static final int MAX_MINUTES = 60; + private static final int MIN_MINUTES = 1; + private static final int MAX_CYCLES = 5; + private static final int MIN_CYCLES = 2; + private static final String ASSERT_STRING_INPUT_NOT_NULL = "String input should not be null!"; + private static final String ERROR_NOT_A_NUMBER = "Invalid payload given in 'config', expected a valid integer!"; + private static final String ERROR_LARGE_CYCLES = "Invalid cycle payload given in 'config', the maximum cycles you " + + "can set is " + MAX_CYCLES + "!"; + private static final String ERROR_LESS_EQUAL_MIN_CYCLES = "Invalid cycle payload given in 'config', the minimum " + + "cycles you can set is " + MIN_CYCLES + "!"; + private static final String ERROR_LARGE_MINUTES = "Invalid minutes payload given in 'config', the maximum time " + + "you can set is " + MAX_MINUTES + "!"; + private static final String ERROR_LESS_EQUAL_MIN_MINUTES = "Invalid minutes payload given in 'config', the minimum " + + "time you can set is " + MIN_MINUTES + "!"; + private static final String ERROR_LONGBREAK_LARGER = "Invalid new 'config'! Your long break time, %s min " + + "should be greater or equal to your " + + "longbreak timing, %s min!"; + private static final String COMMAND_INVALID_ARGUMENTS = "Invalid arguments given to 'config'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'config'!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "config command " + COMMAND_USAGE; + private static final String ASSERT_MISSING_KEYWORD = "Missing command keyword"; + private static final Logger LOGGER = WellNusLogger.getLogger("ConfigCommandLogger"); + private static final String LOG_VALIDATION_ASSUMPTION_FAIL = "New cycle/break/work time is assumed to " + + "have passed the validation bounds and type checking, but has" + + "unexpectedly failed the second redundant check! This may be a developer error."; + private static final String ERROR_SESSION_STARTED = "Cannot config the session as it has already started." + + System.lineSeparator() + + "If you want to reconfigure, `stop` the session and then `config`!"; + + private final FocusUi focusUi; + private final Session session; + private int newCycle; + private int newWork; + private int newBreak; + private int newLongBreak; + + + /** + * Builds an instance of ConfigCommand to allow modification of the common Session attributes + * + * @param arguments Argument-Payload Hashmap generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public ConfigCommand(HashMap arguments, Session session) { + super(arguments); + this.focusUi = new FocusUi(); + this.session = session; + newCycle = session.getCycle(); + newWork = session.getWork(); + newBreak = session.getBrk(); + newLongBreak = session.getLongBrk(); + } + + /** + * Identifies this Command's keyword. Override this in subclasses so + * toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. Override + * this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Executes the specified command from the user.
    + *

    + * May throw Exceptions if command fails. + * + * @throws wellnus.exception.WellNusException If command fails + */ + @Override + public void execute() throws WellNusException { + if (!session.isSessionReady()) { + focusUi.printOutputMessage(ERROR_SESSION_STARTED); + return; + } + HashMap argumentPayloads = getArguments(); + try { + validateCommand(argumentPayloads); + } catch (BadCommandException exception) { + focusUi.printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + return; + } + if (argumentPayloads.size() == COMMAND_MIN_NUM_ARGUMENTS) { + printNewConfiguration(); + return; + } + // Set all the session details as necessary + if (argumentPayloads.containsKey(ARGUMENT_CYCLE)) { + setSessionCycle(argumentPayloads.get(ARGUMENT_CYCLE)); + } + if (argumentPayloads.containsKey(ARGUMENT_WORK)) { + setSessionWork(argumentPayloads.get(ARGUMENT_WORK)); + } + if (argumentPayloads.containsKey(ARGUMENT_BREAK)) { + setSessionBreak(argumentPayloads.get(ARGUMENT_BREAK)); + } + if (argumentPayloads.containsKey(ARGUMENT_LONG_BREAK)) { + setSessionLongBreak(argumentPayloads.get(ARGUMENT_LONG_BREAK)); + } + // Notify the user of the new configuration for user-side verification + printNewConfiguration(); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser.
    + *
    + * The validation logic and strictness is up to the implementer.
    + *
    + * As a guideline, isValidCommand should minimally:
    + *

  • Verify that ALL MANDATORY arguments exist
  • + *
  • Verify that ALL MANDATORY payloads exist
  • + *
  • Safely verify the payload type (int, date, etc should be properly processed)
  • + *
    + * Additionally, payload value cleanup (such as trimming) is also possible.
    + * As Java is pass (object reference) by value, any changes made to commandMap + * will persist out of the function call. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws wellnus.exception.BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(COMMAND_KEYWORD) : ASSERT_MISSING_KEYWORD; + if (arguments.size() > COMMAND_MAX_NUM_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS); + } + if (arguments.size() < COMMAND_MIN_NUM_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS); + } + if (!arguments.get(COMMAND_KEYWORD).equals(EMPTY_STRING)) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + // Validate all the argument payload pairs + for (Map.Entry argumentPair : arguments.entrySet()) { + switch (argumentPair.getKey()) { + case COMMAND_KEYWORD: + continue; + case ARGUMENT_CYCLE: + validateCycles(argumentPair.getValue()); + break; + case ARGUMENT_BREAK: + case ARGUMENT_WORK: + case ARGUMENT_LONG_BREAK: + validateTimes(argumentPair.getValue()); + break; + default: + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS); + } + } + validateLongBreak(arguments); + } + + private void validateLongBreak(HashMap arguments) throws BadCommandException { + int breakTime = this.newBreak; + int longBreakTime = this.newLongBreak; + if (arguments.containsKey(ARGUMENT_BREAK)) { + breakTime = validateTimes(arguments.get(ARGUMENT_BREAK)); + } + if (arguments.containsKey(ARGUMENT_LONG_BREAK)) { + longBreakTime = validateTimes(arguments.get(ARGUMENT_LONG_BREAK)); + } + if (breakTime > longBreakTime) { + String errorMessage = String.format(ERROR_LONGBREAK_LARGER, longBreakTime, breakTime); + throw new BadCommandException(errorMessage); + } + } + + private int getIntegerFromString(String inputString) throws BadCommandException { + assert inputString != null : ASSERT_STRING_INPUT_NOT_NULL; + int result; + try { + result = Integer.parseInt(inputString); + } catch (NumberFormatException exception) { + throw new BadCommandException(ERROR_NOT_A_NUMBER); + } + return result; + } + + private int validateCycles(String cyclePayload) throws BadCommandException { + assert cyclePayload != null : ASSERT_STRING_INPUT_NOT_NULL; + int newCycle = getIntegerFromString(cyclePayload); + if (newCycle > MAX_CYCLES) { + throw new BadCommandException(ERROR_LARGE_CYCLES); + } + if (newCycle < MIN_CYCLES) { + throw new BadCommandException(ERROR_LESS_EQUAL_MIN_CYCLES); + } + return newCycle; + } + + private int validateTimes(String timePayload) throws BadCommandException { + assert timePayload != null : ASSERT_STRING_INPUT_NOT_NULL; + int newTime = getIntegerFromString(timePayload); + if (newTime > MAX_MINUTES) { + throw new BadCommandException(ERROR_LARGE_MINUTES); + } + if (newTime < MIN_MINUTES) { + throw new BadCommandException(ERROR_LESS_EQUAL_MIN_MINUTES); + } + return newTime; + } + + private void setSessionCycle(String sessionCycle) throws FocusException { + // Assume that session cycle must be within the correct range + // Re-run through the validation logic for redundancy & safety + try { + this.newCycle = validateCycles(sessionCycle); + session.setCycle(newCycle); + } catch (BadCommandException exception) { + LOGGER.log(Level.SEVERE, LOG_VALIDATION_ASSUMPTION_FAIL); + throw new FocusException(exception.getMessage()); + } + } + + private void setSessionWork(String sessionWork) throws FocusException { + // Assume that session work must be within the correct range + // Re-run through the validation logic for redundancy & safety + try { + this.newWork = validateTimes(sessionWork); + session.setWork(newWork); + } catch (BadCommandException exception) { + LOGGER.log(Level.SEVERE, LOG_VALIDATION_ASSUMPTION_FAIL); + throw new FocusException(exception.getMessage()); + } + } + + private void setSessionBreak(String sessionBreak) throws FocusException { + // Assume that session break must be within the correct range + // Re-run through the validation logic for redundancy & safety + try { + this.newBreak = validateTimes(sessionBreak); + session.setBrk(newBreak); + } catch (BadCommandException exception) { + LOGGER.log(Level.SEVERE, LOG_VALIDATION_ASSUMPTION_FAIL); + throw new FocusException(exception.getMessage()); + } + } + + private void setSessionLongBreak(String sessionLongBreak) throws FocusException { + // Assume that session work must be within the correct range + // Re-run through the validation logic for redundancy & safety + try { + this.newLongBreak = validateTimes(sessionLongBreak); + session.setLongBrk(newLongBreak); + } catch (BadCommandException exception) { + LOGGER.log(Level.SEVERE, LOG_VALIDATION_ASSUMPTION_FAIL); + throw new FocusException(exception.getMessage()); + } + } + + private void printNewConfiguration() { + String message = PRINT_CONFIG_MESSAGE; + message = message.concat(PRINT_CONFIG_CYCLES + this.newCycle); + message = message.concat(System.lineSeparator()); + message = message.concat(PRINT_CONFIG_WORK + this.newWork); + if (this.newWork == MIN_MINUTES) { + message = message.concat(SINGLE_SPACE_PAD + PRINT_CONFIG_MIN); + } else { + message = message.concat(SINGLE_SPACE_PAD + PRINT_CONFIG_MINS); + } + message = message.concat(System.lineSeparator()); + message = message.concat(PRINT_CONFIG_BREAK + this.newBreak); + if (this.newBreak == MIN_MINUTES) { + message = message.concat(SINGLE_SPACE_PAD + PRINT_CONFIG_MIN); + } else { + message = message.concat(SINGLE_SPACE_PAD + PRINT_CONFIG_MINS); + } + message = message.concat(System.lineSeparator()); + message = message.concat(PRINT_CONFIG_LONG_BREAK + this.newLongBreak); + if (this.newLongBreak == MIN_MINUTES) { + message = message.concat(SINGLE_SPACE_PAD + PRINT_CONFIG_MIN); + } else { + message = message.concat(SINGLE_SPACE_PAD + PRINT_CONFIG_MINS); + } + focusUi.printOutputMessage(message); + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/focus/command/HelpCommand.java b/src/main/java/wellnus/focus/command/HelpCommand.java new file mode 100644 index 0000000000..1c55055f45 --- /dev/null +++ b/src/main/java/wellnus/focus/command/HelpCommand.java @@ -0,0 +1,210 @@ +package wellnus.focus.command; + +import java.util.ArrayList; +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.ui.TextUi; + +/** + * Implementation of Focus Timer WellNus' help command. Explains to the user what commands are supported + * by Focus Timer and how to use each command. + */ +public class HelpCommand extends Command { + public static final String COMMAND_DESCRIPTION = "help - Get help on what commands can be used " + + "in Focus Timer WellNUS++"; + public static final String COMMAND_USAGE = "usage: help [command-to-check]"; + private static final String BAD_ARGUMENTS_MESSAGE = "Invalid arguments given to 'help'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'help'!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "help command " + COMMAND_USAGE; + private static final String COMMAND_KEYWORD = "help"; + private static final String NO_FEATURE_KEYWORD = ""; + private static final String HELP_PREAMBLE = "Input `help` to see all available commands." + + System.lineSeparator() + + "Input `help [command-to-check]` to get usage help for a specific command." + + System.lineSeparator() + + "Here are all the commands available for you!"; + private static final String PADDING = " "; + private static final String DOT = "."; + private static final int ONE_OFFSET = 1; + private static final int EXPECTED_PAYLOAD_SIZE = 1; + private final FocusUi focusUi; + + /** + * Initialises a HelpCommand Object using the command arguments issued by the user. + * + * @param arguments Command arguments issued by the user + */ + public HelpCommand(HashMap arguments) { + super(arguments); + this.focusUi = new FocusUi(); + } + + private TextUi getFocusUi() { + return this.focusUi; + } + + private ArrayList getCommandDescriptions() { + ArrayList commandDescriptions = new ArrayList<>(); + commandDescriptions.add(CheckCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(ConfigCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(HelpCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(HomeCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(NextCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(PauseCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(ResumeCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(StartCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(StopCommand.COMMAND_DESCRIPTION); + return commandDescriptions; + } + + /** + * Prints either the general help message or the command-specific help message + * based on the presence of a payload. + */ + private void printHelpMessage() { + HashMap argumentPayload = getArguments(); + String commandToSearch = argumentPayload.get(COMMAND_KEYWORD).trim().toLowerCase(); + if (commandToSearch.equals(NO_FEATURE_KEYWORD)) { + printGeneralHelpMessage(); + return; + } + printSpecificHelpMessage(commandToSearch); + } + + /** + * Lists all features available in Atomic Habit WellNUS++ and a short description. + */ + public void printGeneralHelpMessage() { + ArrayList commandDescriptions = getCommandDescriptions(); + String outputMessage = FocusManager.FEATURE_HELP_DESCRIPTION; + outputMessage = outputMessage.concat(System.lineSeparator()); + outputMessage = outputMessage.concat(HELP_PREAMBLE); + outputMessage = outputMessage.concat(System.lineSeparator() + System.lineSeparator()); + + for (int i = 0; i < commandDescriptions.size(); i += 1) { + outputMessage = outputMessage.concat(i + ONE_OFFSET + DOT + PADDING); + outputMessage = outputMessage.concat(commandDescriptions.get(i) + System.lineSeparator()); + } + this.getFocusUi().printOutputMessage(outputMessage); + } + + /** + * Prints the help message for a given commandToSearch.
    + * If the commandToSearch does not exist, help will print an unknown command + * error message. + */ + public void printSpecificHelpMessage(String commandToSearch) { + switch (commandToSearch) { + case CheckCommand.COMMAND_KEYWORD: + printUsageMessage(CheckCommand.COMMAND_DESCRIPTION, CheckCommand.COMMAND_USAGE); + break; + case ConfigCommand.COMMAND_KEYWORD: + printUsageMessage(ConfigCommand.COMMAND_DESCRIPTION, ConfigCommand.COMMAND_USAGE); + break; + case HelpCommand.COMMAND_KEYWORD: + printUsageMessage(HelpCommand.COMMAND_DESCRIPTION, HelpCommand.COMMAND_USAGE); + break; + case HomeCommand.COMMAND_KEYWORD: + printUsageMessage(HomeCommand.COMMAND_DESCRIPTION, HomeCommand.COMMAND_USAGE); + break; + case NextCommand.COMMAND_KEYWORD: + printUsageMessage(NextCommand.COMMAND_DESCRIPTION, NextCommand.COMMAND_USAGE); + break; + case PauseCommand.COMMAND_KEYWORD: + printUsageMessage(PauseCommand.COMMAND_DESCRIPTION, PauseCommand.COMMAND_USAGE); + break; + case ResumeCommand.COMMAND_KEYWORD: + printUsageMessage(ResumeCommand.COMMAND_DESCRIPTION, ResumeCommand.COMMAND_USAGE); + break; + case StartCommand.COMMAND_KEYWORD: + printUsageMessage(StartCommand.COMMAND_DESCRIPTION, StartCommand.COMMAND_USAGE); + break; + case StopCommand.COMMAND_KEYWORD: + printUsageMessage(StopCommand.COMMAND_DESCRIPTION, StopCommand.COMMAND_USAGE); + break; + default: + BadCommandException unknownCommand = new BadCommandException(COMMAND_INVALID_PAYLOAD); + focusUi.printErrorFor(unknownCommand, COMMAND_INVALID_COMMAND_NOTE); + } + } + + private void printUsageMessage(String commandDescription, String usageString) { + String message = commandDescription + System.lineSeparator() + usageString; + focusUi.printOutputMessage(message); + } + + @Override + protected String getCommandKeyword() { + return HelpCommand.COMMAND_KEYWORD; + } + + @Override + protected String getFeatureKeyword() { + return HelpCommand.NO_FEATURE_KEYWORD; + } + + /** + * Executes the issued help command.
    + *

    + * Prints a brief description of all of Focus Timer WellNus' supported commands if + * the basic 'help' command was issued.
    + *

    + * Prints a detailed description of a specific feature if the specialised + * 'help' command was issued. + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException exception) { + getFocusUi().printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + return; + } + this.printHelpMessage(); + } + + /** + * Checks whether the given arguments are valid for our help command. + * + * @param arguments Argument-Payload map generated by CommandParser using user's command + * @throws BadCommandException If the command is invalid + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(COMMAND_KEYWORD) : "HelpCommand's payload map does not contain 'help'!"; + // Check if user put in unnecessary payload or arguments + if (arguments.size() > EXPECTED_PAYLOAD_SIZE) { + throw new BadCommandException(BAD_ARGUMENTS_MESSAGE); + } + } + + /** + * Abstract method to ensure developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/focus/command/HomeCommand.java b/src/main/java/wellnus/focus/command/HomeCommand.java new file mode 100644 index 0000000000..1da88c8b65 --- /dev/null +++ b/src/main/java/wellnus/focus/command/HomeCommand.java @@ -0,0 +1,134 @@ +package wellnus.focus.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +/** + * The HomeCommand class is a command class that returns user back to the main WellNUS++ program. + */ +public class HomeCommand extends Command { + public static final String COMMAND_DESCRIPTION = "home - Stop the session and go back to WellNUS++."; + public static final String COMMAND_USAGE = "usage: home"; + public static final String COMMAND_KEYWORD = "home"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'home'!"; + private static final String COMMAND_INVALID_ARGUMENTS = "Invalid arguments given to 'home'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'home'!"; + private static final String HOME_MESSAGE = "Thank you for using focus timer. Keep up the productivity!"; + private final FocusUi focusUi; + private final Session session; + + /** + * Constructor for HomeCommand object. + * Session in FocusManager is passed into this class. + * + * @param arguments Argument-Payload map generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public HomeCommand(HashMap arguments, Session session) { + super(arguments); + this.focusUi = new FocusUi(); + this.session = session; + } + + /** + * Check if a HomeCommand is executed and user wants to return to home. + * + * @param command User command + * @return true If user wants to exit feature false if not + */ + public static boolean isExit(Command command) { + return command instanceof HomeCommand; + } + + /** + * Identifies this Command's keyword. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Stops the current countdown to close background thread. + * Prints the exit feature message for the focus timer feature on the user's screen. + */ + @Override + public void execute() throws WellNusException { + validateCommand(super.getArguments()); + if (!session.isSessionReady()) { + session.getCurrentCountdown().setStop(); + } + // Reset the state of the countdown timer + session.resetCurrentCountdownIndex(); + session.initialiseSession(); + focusUi.printOutputMessage(HOME_MESSAGE); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser. + * User input is valid if no exceptions are thrown. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException if the commandMap has any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() > HomeCommand.COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(HomeCommand.COMMAND_INVALID_ARGUMENTS); + } + if (!arguments.containsKey(HomeCommand.COMMAND_KEYWORD)) { + throw new BadCommandException(HomeCommand.COMMAND_INVALID_COMMAND_MESSAGE); + } + String payload = arguments.get(getCommandKeyword()); + if (!payload.isBlank()) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/focus/command/NextCommand.java b/src/main/java/wellnus/focus/command/NextCommand.java new file mode 100644 index 0000000000..7dd5cadcd8 --- /dev/null +++ b/src/main/java/wellnus/focus/command/NextCommand.java @@ -0,0 +1,137 @@ +package wellnus.focus.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +/** + * Represents a class to start the next countdown in the session. + */ +public class NextCommand extends Command { + + public static final String COMMAND_DESCRIPTION = "next - Move on to the next countdown. " + + "Can only be used when a countdown timer has ended."; + public static final String COMMAND_USAGE = "usage: next"; + public static final String COMMAND_KEYWORD = "next"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'next'!"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'next'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'next'!"; + private static final String ERROR_SESSION_NOT_STARTED = "A focus session has not started yet, " + + "try `start`ing one first!"; + private static final String ERROR_COUNTDOWN_RUNNING = "Oops, your timer for this session is still ticking!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "next command " + COMMAND_USAGE; + private final Session session; + private final FocusUi focusUi; + + /** + * Constructor for NextCommand object. + * Session in FocusManager is passed into this class. + * + * @param arguments Argument-Payload Hashmap generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public NextCommand(HashMap arguments, Session session) { + super(arguments); + this.session = session; + this.focusUi = new FocusUi(); + } + + /** + * Identifies this Command's keyword. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Outputs unique description of the countdown. + * Starts the session by starting the first countdown. + * If the session has already started, it will start the next countdown. + */ + @Override + public void execute() { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + focusUi.printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + if (session.isSessionReady()) { + focusUi.printOutputMessage(ERROR_SESSION_NOT_STARTED); + return; + } + if (session.isSessionCounting() || session.isSessionPaused()) { + focusUi.printOutputMessage(ERROR_COUNTDOWN_RUNNING); + return; + } + session.startTimer(); + focusUi.printOutputMessage(session.getCurrentCountdown().getDescription()); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser. + * User input is valid if no exceptions are thrown. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(COMMAND_INVALID_COMMAND_MESSAGE); + } + if (!arguments.get(COMMAND_KEYWORD).equals("")) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} + diff --git a/src/main/java/wellnus/focus/command/PauseCommand.java b/src/main/java/wellnus/focus/command/PauseCommand.java new file mode 100644 index 0000000000..7d391e3cb1 --- /dev/null +++ b/src/main/java/wellnus/focus/command/PauseCommand.java @@ -0,0 +1,138 @@ +package wellnus.focus.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +/** + * Represents a class to pause the current countdown in the session. + */ +public class PauseCommand extends Command { + public static final String COMMAND_DESCRIPTION = "pause - Pause the session! " + + "Can only be used when a countdown is ticking."; + public static final String COMMAND_USAGE = "usage: pause"; + public static final String COMMAND_KEYWORD = "pause"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'pause'!"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'pause'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'pause'!"; + private static final String PAUSE_OUTPUT = "Timer paused at: "; + private static final String ERROR_COUNTDOWN_NOT_RUNNING = "Nothing to pause - the timer has not started yet!"; + private static final String ERROR_IS_PAUSED = "Nothing to pause - you have already paused the timer!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "pause command " + COMMAND_USAGE; + private final Session session; + private final FocusUi focusUi; + + /** + * Constructor for PauseCommand object. + * Session in FocusManager is passed into this class. + * + * @param arguments Argument-Payload Hashmap generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public PauseCommand(HashMap arguments, Session session) { + super(arguments); + this.session = session; + this.focusUi = new FocusUi(); + } + + /** + * Identifies this Command's keyword. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Pause the current countdown. + * Prints the time left in the current countdown for user to view. + */ + @Override + public void execute() { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + focusUi.printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + // Only execute the pause logic if the countdown is not running + if (session.isSessionPaused()) { + focusUi.printOutputMessage(ERROR_IS_PAUSED); + return; + } + if (!session.isSessionCounting()) { + // Gently tell the user why pause did not execute + focusUi.printOutputMessage(ERROR_COUNTDOWN_NOT_RUNNING); + return; + } + session.getCurrentCountdown().setPause(); + int minutes = session.getCurrentCountdown().getMinutes(); + int seconds = session.getCurrentCountdown().getSeconds(); + focusUi.printOutputMessage(PAUSE_OUTPUT + String.format("%d:%d", minutes, seconds)); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser. + * User input is valid if no exceptions are thrown. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(COMMAND_INVALID_COMMAND_MESSAGE); + } + if (!arguments.get(COMMAND_KEYWORD).equals("")) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/focus/command/ResumeCommand.java b/src/main/java/wellnus/focus/command/ResumeCommand.java new file mode 100644 index 0000000000..83d946c261 --- /dev/null +++ b/src/main/java/wellnus/focus/command/ResumeCommand.java @@ -0,0 +1,130 @@ +package wellnus.focus.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +/** + * Represents a command to resume the countdown timer in the current session. + */ +public class ResumeCommand extends Command { + + public static final String COMMAND_DESCRIPTION = "resume - Continue the countdown. " + + "Can only be used when a countdown is paused."; + public static final String COMMAND_USAGE = "usage: home"; + public static final String COMMAND_KEYWORD = "resume"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'resume'!"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'resume'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'resume'!"; + private static final String COMMAND_KEYWORD_ASSERTION = "The key should be resume."; + private static final String RESUME_OUTPUT = "Timer resumed at: "; + private static final String ERROR_NOT_PAUSED = "You don't seem to be paused. Ignoring the command!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "resume command " + COMMAND_USAGE; + private final Session session; + private final FocusUi focusUi; + + /** + * Constructs a ResumeCommand object. + * Session in FocusManager is passed into this class. + * + * @param arguments Argument-Payload Hashmap generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public ResumeCommand(HashMap arguments, Session session) { + super(arguments); + this.session = session; + this.focusUi = new FocusUi(); + } + + /** + * Identifies this Command's keyword. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Resumes the current countdown. + * Prints the time left to be completed in the current countdown. + */ + @Override + public void execute() { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + focusUi.printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + assert super.getArguments().containsKey(COMMAND_KEYWORD) : COMMAND_KEYWORD_ASSERTION; + if (!session.hasAnyCountdown() || !session.isSessionPaused()) { + focusUi.printOutputMessage(ERROR_NOT_PAUSED); + return; + } + int minutes = session.getCurrentCountdown().getMinutes(); + int seconds = session.getCurrentCountdown().getSeconds(); + focusUi.printOutputMessage(RESUME_OUTPUT + String.format("%d:%d", minutes, seconds)); + session.getCurrentCountdown().setStart(); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser. + * User input is valid if no arguments are thrown. + */ + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(COMMAND_INVALID_COMMAND_MESSAGE); + } + if (!arguments.get(COMMAND_KEYWORD).equals("")) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/focus/command/StartCommand.java b/src/main/java/wellnus/focus/command/StartCommand.java new file mode 100644 index 0000000000..42babe2960 --- /dev/null +++ b/src/main/java/wellnus/focus/command/StartCommand.java @@ -0,0 +1,133 @@ +package wellnus.focus.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +/** + * Represents a command to start the current session. + * Also used to start different countdowns timers in the session. + */ +public class StartCommand extends Command { + public static final String COMMAND_DESCRIPTION = "start - Start your focus session!"; + public static final String COMMAND_USAGE = "usage: start"; + public static final String COMMAND_KEYWORD = "start"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final int FIRST_COUNTDOWN_INDEX = 0; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'start'!"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'start'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'start'!"; + private static final String START_MESSAGE = "Your session has started. All the best!"; + private static final String ERROR_NOT_READY = "Nothing to start - your session has started!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "start command " + COMMAND_USAGE; + private final Session session; + private final FocusUi focusUi; + + /** + * Constructor for StartCommand object. + * Session in FocusManager is passed into this class. + * + * @param arguments Argument-Payload Hashmap generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public StartCommand(HashMap arguments, Session session) { + super(arguments); + this.session = session; + this.focusUi = new FocusUi(); + } + + /** + * Identifies this Command's keyword. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Outputs unique description of the countdown. + * Starts the session by starting the first countdown. + * If the session has already started, it will start the next countdown. + */ + @Override + public void execute() { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + focusUi.printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + if (!session.isSessionReady()) { + focusUi.printOutputMessage(ERROR_NOT_READY); + return; + } + // Forcefully initialise the session again for repeated countdowns + focusUi.printOutputMessage(START_MESSAGE); + session.startTimer(); + focusUi.printOutputMessage(session.getSession().get(FIRST_COUNTDOWN_INDEX).getDescription()); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser. + * User input is valid if no exceptions are thrown. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(COMMAND_INVALID_COMMAND_MESSAGE); + } + if (!arguments.get(COMMAND_KEYWORD).equals("")) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/focus/command/StopCommand.java b/src/main/java/wellnus/focus/command/StopCommand.java new file mode 100644 index 0000000000..73705f65aa --- /dev/null +++ b/src/main/java/wellnus/focus/command/StopCommand.java @@ -0,0 +1,137 @@ +package wellnus.focus.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +/** + * Represents a command to stop the current session. + */ +public class StopCommand extends Command { + public static final String COMMAND_DESCRIPTION = "stop - Stop the session. You will have to `start` " + + "your focus session again!"; + public static final String COMMAND_USAGE = "usage: stop"; + public static final String COMMAND_KEYWORD = "stop"; + private static final int COMMAND_NUM_OF_ARGUMENTS = 1; + private static final String COMMAND_INVALID_COMMAND_MESSAGE = "Invalid command issued, expected 'stop'!"; + private static final String COMMAND_INVALID_ARGUMENTS_MESSAGE = "Invalid arguments given to 'stop'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'stop'!"; + private static final String STOP_MESSAGE = "Your focus session has ended." + + System.lineSeparator() + + "To start a new session, `start` it up!" + + System.lineSeparator() + + "You can also configure the session to your liking with `config`!"; + private static final String ERROR_NOT_STARTED = "Nothing to stop - the timer has not started yet!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "stop command " + COMMAND_USAGE; + private final Session session; + private final FocusUi focusUi; + + /** + * Constructs a StopCommand object. + * Session in FocusManager is passed into this class. + * + * @param arguments Argument-Payload Hashmap generated by CommandParser + * @param session Session object which is an arraylist of Countdowns + */ + public StopCommand(HashMap arguments, Session session) { + super(arguments); + this.session = session; + this.focusUi = new FocusUi(); + } + + /** + * Identifies this Command's keyword. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword of this Command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Identifies the feature that this Command is associated with. + * Override this in subclasses so toString() returns the correct String representation. + * + * @return String Keyword for the feature associated with this Command + */ + @Override + protected String getFeatureKeyword() { + return FocusManager.FEATURE_NAME; + } + + /** + * Prints message to indicate session has ended. + * Stops the session. + * Resets the current countdown index. + */ + @Override + public void execute() { + try { + validateCommand(super.getArguments()); + } catch (BadCommandException badCommandException) { + focusUi.printErrorFor(badCommandException, COMMAND_INVALID_COMMAND_NOTE); + return; + } + if (!session.hasAnyCountdown() || session.isSessionReady()) { + focusUi.printOutputMessage(ERROR_NOT_STARTED); + return; + } + focusUi.printOutputMessage(STOP_MESSAGE); + session.getCurrentCountdown().setStop(); + session.initialiseSession(); + session.resetCurrentCountdownIndex(); + } + + /** + * Validate the arguments and payloads from a commandMap generated by CommandParser. + * User input is valid if no exceptions are thrown. + * + * @param arguments Argument-Payload map generated by CommandParser + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + if (arguments.size() != COMMAND_NUM_OF_ARGUMENTS) { + throw new BadCommandException(COMMAND_INVALID_ARGUMENTS_MESSAGE); + } + if (!arguments.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(COMMAND_INVALID_COMMAND_MESSAGE); + } + if (!arguments.get(COMMAND_KEYWORD).equals("")) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} + diff --git a/src/main/java/wellnus/focus/feature/Countdown.java b/src/main/java/wellnus/focus/feature/Countdown.java new file mode 100644 index 0000000000..ea49699225 --- /dev/null +++ b/src/main/java/wellnus/focus/feature/Countdown.java @@ -0,0 +1,233 @@ +package wellnus.focus.feature; + + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * Class to represent a timer counting down given a specific minutes. + * This class contains a Timer class which is used to simulate a clock counting down. + * Atomic data type is used to communicate with the timer background thread. + */ +public class Countdown { + private static final int ONE_SECOND = 1000; + private static final int DELAY_TIME = 0; + private static final int DEFAULT_STOP_TIME = 0; + private static final int DEFAULT_SECONDS = 59; + private static final int INITIAL_SECONDS = 0; + private static final int COUNTDOWN_PRINT_START_TIME = 10; + private static final String MINUTES_INPUT_ASSERTION = "Minutes should be greater than 0"; + private static final String STOP_BEFORE_START_ASSERTION = "Timer should be started before trying to stop it"; + private static final String TIMER_NOT_RUNNING_ASSERTION = "Timer should not be running"; + private static final String TIMER_COMPLETE_MESSAGE = "Type 'next' to begin the next countdown"; + private static final String TIMER_COMPLETE_MESSAGE_LAST = "Congrats! That's a session done and dusted!" + + System.lineSeparator() + + "Type `start` to start a new session, or `config` to change the session settings."; + private static final String FEATURE_NAME = "ft"; + private FocusUi focusUi; + private Timer timer; + private int minutes; + private int inputMinutes; + private int seconds; + private final String description; + private boolean isLast; + // Convenience attribute to signify that this countdown object is the rollover countdown + private boolean isReady = false; + private AtomicBoolean isCompletedCountdown; + private AtomicBoolean isRunClock; + + /** + * Constructor of Countdown. + * This will initialise the private attributes of Countdown object. + * + * @param minutes the number of minutes to countdown + * @param description description of the current task user is focusing on + */ + public Countdown(int minutes, String description, boolean isLast) { + assert minutes > 0 : MINUTES_INPUT_ASSERTION; + this.minutes = minutes; + this.inputMinutes = minutes; + this.seconds = INITIAL_SECONDS; + this.isCompletedCountdown = new AtomicBoolean(false); + this.isRunClock = new AtomicBoolean(false); + this.description = description; + this.focusUi = new FocusUi(); + focusUi.setCursorName(FEATURE_NAME); + this.isLast = isLast; + } + + /** + * This method will execute the actions necessary when a countdown completes. + * The timer will be stopped and a beep sound will be played. + * A message will be printed to the user to notify them that the countdown has completed. + */ + private void timerComplete() { + setStop(); + java.awt.Toolkit.getDefaultToolkit().beep(); + if (isLast) { + focusUi.printOutputMessage(TIMER_COMPLETE_MESSAGE_LAST); + } else { + focusUi.printOutputMessage(TIMER_COMPLETE_MESSAGE); + } + this.minutes = inputMinutes; + this.isCompletedCountdown.set(true); + if (isLast) { + setIsReady(true); + } + focusUi.printCursor(); + } + + /** + * This method will decrement the minutes by 1; + */ + private void decrementMinutes() { + minutes--; + } + + /** + * This method will decrement the seconds by 1; + */ + private void decrementSeconds() { + seconds--; + } + + /** + * This method will start the countdown timer. + * Timer will notify user when the countdown timer has completed by playing a beep sound. + * The timer will run in the background and will be stopped when the countdown minutes and seconds reaches 0. + */ + public void start() { + assert isRunClock.get() == false : TIMER_NOT_RUNNING_ASSERTION; + timer = new Timer(); + TimerTask countdownTask = new TimerTask() { + @Override + public void run() { + setIsReady(false); + if (!isRunClock.get()) { + return; + } + if (minutes == DEFAULT_STOP_TIME && seconds == COUNTDOWN_PRINT_START_TIME) { + focusUi.printNewline(); + } + if (isCountdownPrinting()) { + focusUi.printOutputMessage(seconds + " seconds left"); + } + if (seconds == DEFAULT_STOP_TIME && minutes == DEFAULT_STOP_TIME) { + timerComplete(); + } else if (seconds == DEFAULT_STOP_TIME) { + seconds = DEFAULT_SECONDS; + decrementMinutes(); + } else { + decrementSeconds(); + } + } + }; + timer.scheduleAtFixedRate(countdownTask, DELAY_TIME, ONE_SECOND); + } + + /** + * Utility method to check if the countdown is in its printing phase.
    + *

    + * Used to determine whether to print the seconds left and accept any user input. + * + * @return boolean Representing if the countdown timer is printing. + */ + public boolean isCountdownPrinting() { + return (minutes == DEFAULT_STOP_TIME + && seconds <= COUNTDOWN_PRINT_START_TIME + && seconds != DEFAULT_STOP_TIME); + } + + /** + * This method will return the status of the countdown timer. + * + * @return true if the countdown timer has completed, false otherwise + */ + public boolean getIsCompletedCountdown() { + return isCompletedCountdown.get(); + } + + /** + * This method will stop the countdown timer and stops the background thread. + */ + public void setStop() { + // timer is only initialised in start() method, so calling setStop() leads + // to a crash. Catch this mistake with an assertion + assert timer != null : STOP_BEFORE_START_ASSERTION; + isCompletedCountdown.set(true); + isRunClock.set(false); + timer.cancel(); + timer.purge(); + } + + /** + * This method will allow the countdown timer to count down. + * It does so by allowing the minutes and seconds to be decremented. + */ + public void setStart() { + isRunClock.set(true); + } + + /** + * This method will pause the countdown timer. + */ + public void setPause() { + isRunClock.set(false); + } + + /** + * This method will return the status of the countdown timer. + * + * @return true if the countdown timer is running, false otherwise + */ + public boolean getIsRunning() { + return isRunClock.get(); + } + + /** + * This method will return the current minutes of the countdown timer. + * + * @return the minutes of the countdown timer + */ + public int getMinutes() { + return this.minutes; + } + + /** + * This method will return the current seconds of the countdown timer. + * + * @return the seconds of the countdown timer + */ + public int getSeconds() { + return this.seconds; + } + + /** + * This method will return the description of the countdown timer. + * + * @return the description of the countdown timer + */ + public String getDescription() { + return this.description; + } + + /** + * This method will return the ready status of the session + *

    + * Only the last countdown timer object in a session can have this as true. + * The last countdown timer object will be 'ready' only if it is not counting down. + * This is meant to help the Session manage starting new sessions. + * + * @return boolean representing the ready state of the session + */ + public boolean getIsReady() { + return isReady; + } + + public void setIsReady(boolean isReady) { + this.isReady = isReady; + } + +} diff --git a/src/main/java/wellnus/focus/feature/FocusManager.java b/src/main/java/wellnus/focus/feature/FocusManager.java new file mode 100644 index 0000000000..b5dd7a42f8 --- /dev/null +++ b/src/main/java/wellnus/focus/feature/FocusManager.java @@ -0,0 +1,211 @@ +package wellnus.focus.feature; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; +import wellnus.focus.command.CheckCommand; +import wellnus.focus.command.ConfigCommand; +import wellnus.focus.command.HelpCommand; +import wellnus.focus.command.HomeCommand; +import wellnus.focus.command.NextCommand; +import wellnus.focus.command.PauseCommand; +import wellnus.focus.command.ResumeCommand; +import wellnus.focus.command.StartCommand; +import wellnus.focus.command.StopCommand; +import wellnus.manager.Manager; + +/** + * Represents a class to run the event driver for the Focus Timer. + * This class will be called by the main manager. + * It will match the user input to the correct command and execute it. + */ +//@@author YongbinWang +public class FocusManager extends Manager { + + public static final String FEATURE_HELP_DESCRIPTION = "ft - Focus Timer - Set a configurable 'Pomodoro' timer " + + "with work and rest cycles to keep yourself focused and productive!"; + public static final String FEATURE_NAME = "ft"; + private static final String START_COMMAND_KEYWORD = "start"; + private static final String PAUSE_COMMAND_KEYWORD = "pause"; + private static final String RESUME_COMMAND_KEYWORD = "resume"; + private static final String NEXT_COMMAND_KEYWORD = "next"; + private static final String CONFIG_COMMAND_KEYWORD = "config"; + private static final String HOME_COMMAND_KEYWORD = "home"; + private static final String STOP_COMMAND_KEYWORD = "stop"; + private static final String CHECK_COMMAND_KEYWORD = "check"; + private static final String HELP_COMMAND_KEYWORD = "help"; + private static final String UNKNOWN_COMMAND_MESSAGE = "Invalid command issued!"; + private static final String FOCUS_TIMER_GREET = "Welcome to Focus Timer." + System.lineSeparator() + + "Start a focus session with `start`, or `config` the session first!"; + private static final String FOCUS_GREETING_LOGO = " /$$$$$$$$ " + + System.lineSeparator() + + + "| $$_____/ " + System.lineSeparator() + + + "| $$ /$$$$$$ /$$$$$$$ /$$ /$$ /$$$$$$$" + System.lineSeparator() + + + "| $$$$$ /$$__ $$ /$$_____/| $$ | $$ /$$_____/ " + System.lineSeparator() + + + "| $$__/| $$ \\ $$| $$ | $$ | $$| $$$$$$ " + System.lineSeparator() + + + "| $$ | $$ | $$| $$ | $$ | $$ \\____ $$" + System.lineSeparator() + + + "| $$ | $$$$$$/| $$$$$$$| $$$$$$/ /$$$$$$$/" + System.lineSeparator() + + + "|__/ \\______/ \\_______/ \\______/ |_______/" + System.lineSeparator(); + private static final String COMMAND_KEYWORD_ASSERTION = "The key cannot be null" + + ", check user-guide for valid commands"; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String COMMAND_INVALID_COMMAND_NOTE = "Supported commands in Focus Timer: " + LINE_SEPARATOR + + "check command " + CheckCommand.COMMAND_USAGE + LINE_SEPARATOR + + "config command " + ConfigCommand.COMMAND_USAGE + LINE_SEPARATOR + + "next command " + NextCommand.COMMAND_USAGE + LINE_SEPARATOR + + "pause command " + PauseCommand.COMMAND_USAGE + LINE_SEPARATOR + + "resume command " + ResumeCommand.COMMAND_USAGE + LINE_SEPARATOR + + "start command " + StartCommand.COMMAND_USAGE + LINE_SEPARATOR + + "stop command " + StopCommand.COMMAND_USAGE + LINE_SEPARATOR + + "help command " + HelpCommand.COMMAND_USAGE + LINE_SEPARATOR + + "home command " + HomeCommand.COMMAND_USAGE; + private static final String HOME_USAGE = "home command " + HomeCommand.COMMAND_USAGE; + private static final String CONFIG_USAGE = "config command " + ConfigCommand.COMMAND_USAGE; + private final FocusUi focusUi; + private final Session session; + + /** + * Constructs a FocusManager object. + * Initialise a session and textUi. + * Session will be passed into different commands to be utilised. + */ + public FocusManager() { + this.focusUi = new FocusUi(); + this.focusUi.setCursorName(FEATURE_NAME); + this.session = new Session(); + } + + /** + * Parses the given command from the user. + * Determines the correct Command subclass that can handle its execution. + * + * @param commandString Full command issued by the user + * @return Command object that can execute the user's command + * @throws BadCommandException If an unknown command was issued by the user + */ + private Command getCommandFor(String commandString) throws BadCommandException { + HashMap arguments = getCommandParser().parseUserInput(commandString); + String commandKeyword = getCommandParser().getMainArgument(commandString); + assert commandKeyword != null : COMMAND_KEYWORD_ASSERTION; + switch (commandKeyword) { + case START_COMMAND_KEYWORD: + return new StartCommand(arguments, session); + case PAUSE_COMMAND_KEYWORD: + return new PauseCommand(arguments, session); + case RESUME_COMMAND_KEYWORD: + return new ResumeCommand(arguments, session); + case HOME_COMMAND_KEYWORD: + return new HomeCommand(arguments, session); + case STOP_COMMAND_KEYWORD: + return new StopCommand(arguments, session); + case CHECK_COMMAND_KEYWORD: + return new CheckCommand(arguments, session); + case NEXT_COMMAND_KEYWORD: + return new NextCommand(arguments, session); + case CONFIG_COMMAND_KEYWORD: + return new ConfigCommand(arguments, session); + case HELP_COMMAND_KEYWORD: + return new HelpCommand(arguments); + default: + throw new BadCommandException(UNKNOWN_COMMAND_MESSAGE); + } + } + + private void greet() { + focusUi.printLogoWithSeparator(FOCUS_GREETING_LOGO); + focusUi.printOutputMessage(FOCUS_TIMER_GREET); + } + + private void runCommands() { + boolean isExit = false; + while (!isExit) { + // Ignore ALL user input if the command is in its printing phase + try { + String commandString = focusUi.getCommand(session); + // Edge case guard clause to ensure that + Command command = getCommandFor(commandString); + command.execute(); + isExit = HomeCommand.isExit(command); + } catch (WellNusException exception) { + String errorMessage = exception.getMessage(); + if (errorMessage.contains(CONFIG_COMMAND_KEYWORD)) { + focusUi.printErrorFor(exception, CONFIG_USAGE); + } else if (errorMessage.contains(HOME_COMMAND_KEYWORD)) { + focusUi.printErrorFor(exception, HOME_USAGE); + } else { + focusUi.printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + } + } + } + } + + /** + * Utility function to get the featureName this Manager is administering. + * + * @return name of the feature that this Manager handles + */ + @Override + public String getFeatureName() { + return FEATURE_NAME; + } + + /** + * runEventDriver is the entry point into a feature's driver loop. + */ + @Override + public void runEventDriver() throws BadCommandException { + greet(); + runCommands(); + } + + /** + * Public method used for testing FocusManager's handling of invalidCommands. + * + * @param userCommand simulated user input + * @return Command object that can execute the user's command + * @throws BadCommandException + */ + public Command testInvalidCommand(String userCommand) throws BadCommandException { + String startCommand = "focusStart"; + String pauseCommand = "focusPause"; + String resumeCommand = "focusResume"; + String homeCommand = "focusHome"; + String stopCommand = "focusStop"; + String checkCommand = "focusCheck"; + HashMap arguments; + switch (userCommand) { + case START_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(startCommand); + return new StartCommand(arguments, session); + case PAUSE_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(pauseCommand); + return new PauseCommand(arguments, session); + case RESUME_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(resumeCommand); + return new ResumeCommand(arguments, session); + case HOME_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(homeCommand); + return new HomeCommand(arguments, session); + case STOP_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(stopCommand); + return new StopCommand(arguments, session); + case CHECK_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(checkCommand); + return new CheckCommand(arguments, session); + case CONFIG_COMMAND_KEYWORD: + arguments = getCommandParser().parseUserInput(checkCommand); + return new ConfigCommand(arguments, session); + default: + throw new BadCommandException(UNKNOWN_COMMAND_MESSAGE); + } + } +} diff --git a/src/main/java/wellnus/focus/feature/FocusUi.java b/src/main/java/wellnus/focus/feature/FocusUi.java new file mode 100644 index 0000000000..d8092edb1b --- /dev/null +++ b/src/main/java/wellnus/focus/feature/FocusUi.java @@ -0,0 +1,155 @@ +package wellnus.focus.feature; + +import java.nio.BufferOverflowException; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.common.WellNusLogger; +import wellnus.ui.TextUi; + +/** + * FocusUi contains extra logic to handle special cursor-printing logic and reading of stdin + * due to the need to block all user input when the countdown timer starts counting down from + * (10,9,...,1). + */ +public class FocusUi extends TextUi { + private static final Logger LOGGER = WellNusLogger.getLogger("FocusUiLogger"); + private static final String NO_INPUT_ELEMENT_MSG = "There is no new line of element," + + "please key in your input!!"; + private static final String BUFFER_OVERFLOW_MSG = "Your input is too long," + + "please shorten it!!"; + private static final String SEPARATOR = "*"; + private static final int FLUSH_DELAY_TIMING = 1000; + private static final boolean NO_PRINT_CURSOR = false; + private static final boolean PRINT_CURSOR = true; + + + /** + * Constructs a FocusUi variant of TextUi + */ + public FocusUi() { + super(); + setSeparator(SEPARATOR); + } + + /** + * FocusUi specific getCommand that accounts for proper printing of + * the cursor and accepting user input when the countdown printing phase (10,9,...,1) + * is not ongoing. + *

    + * In this scenario, we define invalid phase to be the point where + * the countdown prints (10,9,...,1) on screen. + * Otherwise, it is valid. + * + * @return User input command with leading/dangling whitespace being removed + */ + public String getCommand(Session session) { + Scanner scanner = getScanner(); + // User tries to input a command in the invalid phase + if (isBlocking(session)) { + return waitAndGetCommand(session, scanner); + } + // User entered command during valid phase + String userCommand = getCommandWithCursor(PRINT_CURSOR, scanner); + // Edge case: User maliciously waits until the countdown print starts before pressing enter. + // In this case, the command should NOT be processed until the countdown phase is over. + if (isBlocking(session)) { + return waitAndGetCommand(session, scanner); + } + return userCommand; + } + + private void printLogo(String logo) { + System.out.print(logo); + } + + protected void printLogoWithSeparator(String logo) { + printSeparator(); + printLogo(logo); + } + + /** + * User has entered a command whilst the countdown printing phase is ongoing. + *

    + * Waits for the printing phase to end, flush stdin and then gets the command without printing a cursor + * as the Countdown will automatically print a cursor on completion. + * + * @param session the session containing all the countdowns + * @param scanner the scanner reading user input + * @return String User input command with leading/dangling whitespace being removed + */ + private String waitAndGetCommand(Session session, Scanner scanner) { + waitForCountdownPrint(session); + flushInput(scanner); + // Get command without printing a cursor + return getCommandWithCursor(NO_PRINT_CURSOR, scanner); + } + + /** + * This method is the lowest abstraction layer, talking to + * + * @param isPrintCursor the session containing all the countdowns + * @param scanner the scanner reading user input + * @return User input command with leading/dangling whitespace being removed + */ + private String getCommandWithCursor(boolean isPrintCursor, Scanner scanner) { + String userCommand = ""; + if (isPrintCursor) { + printCursor(); + } + try { + String inputLine = scanner.nextLine(); + userCommand = inputLine.trim(); + } catch (BufferOverflowException bufferOverFlowException) { + LOGGER.log(Level.INFO, BUFFER_OVERFLOW_MSG); + printErrorFor(bufferOverFlowException, BUFFER_OVERFLOW_MSG); + } catch (NoSuchElementException noElementException) { + LOGGER.log(Level.INFO, NO_INPUT_ELEMENT_MSG); + printErrorFor(noElementException, NO_INPUT_ELEMENT_MSG); + } + return userCommand; + } + + /** + * Checks if the countTimer is in an invalid phase which + * means that all stdin should be blocked. + * + * @param session the session containing all the countdowns + * @return boolean Representing the countdown state + */ + public boolean isBlocking(Session session) { + return session.getCurrentCountdown().isCountdownPrinting() && session.isSessionCounting(); + } + + /** + * If the countdown is in its printing phase, wait for it to finish first + * The while loop is designed as such for checkstyle correctness (no empty body) + */ + private void waitForCountdownPrint(Session session) { + while (true) { + if (!(isBlocking(session))) { + break; + } + } + } + + /** + * Flush all input from Stdin. + *

    + * Gracefully rejects all overzealous input from users, cleaning the input buffer. + * + * @param scanner Reference to the scanner being used by the UI + */ + private void flushInput(Scanner scanner) { + // Implement a timeout to avoid hanging + long flushStartTime = System.currentTimeMillis(); + while (scanner.hasNextLine() + && System.currentTimeMillis() - flushStartTime < FLUSH_DELAY_TIMING) { + // Discard extraneous input + scanner.nextLine(); + } + } + +} diff --git a/src/main/java/wellnus/focus/feature/Session.java b/src/main/java/wellnus/focus/feature/Session.java new file mode 100644 index 0000000000..0abb5e38ab --- /dev/null +++ b/src/main/java/wellnus/focus/feature/Session.java @@ -0,0 +1,226 @@ +package wellnus.focus.feature; + +import java.util.ArrayList; + +//@@author YongbinWang + +/** + * Represents a session of Countdown objects. + * A session is a sequence of Countdown objects represented by an ArrayList. + * We define a Session to have 4 states. + *

      + *
    1. Ready + * `config` and `start` are only legal here. + *
    2. Counting + * `pause` and `stop` are only legal here. + *
    3. Waiting + * `next` and `stop` are only legal here. + *
    4. Paused + * `resume` and `stop` are only legal here. + *
    + * The last timer holds a special `isReady` attribute to help Session determine if `start` and `config` is usable. + */ +public class Session { + private static final int INCREMENT = 1; + private static final boolean IS_LAST_COUNTDOWN = true; + private String workDescription = "Task Cycle: Do your task now!"; + private final ArrayList session; + private String breakDescription = "Break Cycle: Take a breather!"; + private String longBreakDescription = "Long Break"; + private int work = 1; + private int brk = 1; + private int longBrk = 1; + private int cycle = 2; + private int currentCountdownIndex; + + /** + * Constructs a Session object. + * Creates an ArrayList of Countdown objects. + * Calls fillSession() to fill the session with Countdown objects. + */ + public Session() { + this.session = new ArrayList<>(); + initialiseSession(); + this.currentCountdownIndex = session.size() - INCREMENT; + } + + /** + * Method to fill the session with Countdown objects in a specific sequence. + */ + private void fillSession() { + for (int i = 0; i < cycle; i++) { + Countdown workCountDown = new Countdown(work, workDescription, !IS_LAST_COUNTDOWN); + Countdown breakCountDown = new Countdown(brk, breakDescription, !IS_LAST_COUNTDOWN); + session.add(workCountDown); + session.add(breakCountDown); + } + Countdown longBreak = new Countdown(longBrk, longBreakDescription, IS_LAST_COUNTDOWN); + int lastIndex = session.size() - 1; + session.remove(lastIndex); + session.add(longBreak); + } + + /** + * Method to get the session. + * + * @return arraylist of countdown objects + */ + public ArrayList getSession() { + return this.session; + } + + /** + * Method to get the current countdown index. + * + * @return the current countdown index + */ + public int getCurrentCountdownIndex() { + return this.currentCountdownIndex; + } + + //@@author nichyjt + + /** + * Method to increment the current countdown index if the current countdown is completed. + */ + public void checkPrevCountdown() { + if (getCurrentCountdown().getIsReady()) { + initialiseSession(); + currentCountdownIndex = 0; + return; + } + currentCountdownIndex += INCREMENT; + } + + /** + * Get the current countdown object actively ticking in the Session + * + * @return Countdown the current countdown being ticked + */ + public Countdown getCurrentCountdown() { + return session.get(currentCountdownIndex); + } + + /** + * Check if the session is in its Ready state. + *

    + * A session can only be ready if the current index is the last timer + * and the last timer's isReady is true, which can only occur if it is not counting down. + * + * @return boolean Representing the ready state of the session. + */ + public boolean isSessionReady() { + return getCurrentCountdownIndex() == session.size() - INCREMENT && getCurrentCountdown().getIsReady(); + } + + /** + * Check if the session is in its Counting state. + * + * @return boolean Representing if the session's countdown is underway. + */ + public boolean isSessionCounting() { + Countdown countdown = getCurrentCountdown(); + return countdown.getIsRunning() && !countdown.getIsCompletedCountdown(); + } + + /** + * Check if the session is in its Waiting state. + * + * @return boolean Representing if the session's countdown is done and is waiting for a next command. + */ + public boolean isSessionWaiting() { + Countdown countdown = getCurrentCountdown(); + return !countdown.getIsRunning() && countdown.getIsCompletedCountdown(); + } + + /** + * Check if the session is in its Paused state. + * + * @return boolean Representing if the session's countdown is paused. + */ + public boolean isSessionPaused() { + Countdown countdown = getCurrentCountdown(); + return !countdown.getIsRunning() && !countdown.getIsCompletedCountdown() && !countdown.getIsReady(); + } + + /** + * Starts the timer for the current countdown and increment the index if needed. + */ + public void startTimer() { + checkPrevCountdown(); + getCurrentCountdown().start(); + getCurrentCountdown().setStart(); + } + + /** + * Method to (re) initialise a session when start or stop command is executed. + */ + public void initialiseSession() { + session.clear(); + fillSession(); + primeSessionIsReady(); + } + + /** + * Sets the isReady flag in the session for the last countdown object to be true + */ + private void primeSessionIsReady() { + int lastIndex = session.size() - INCREMENT; + session.get(lastIndex).setIsReady(true); + } + + //@@author YongbinWang + + /** + * Check if the session has any countdowns in its list + * + * @return boolean Representing if there is any countdown + */ + public boolean hasAnyCountdown() { + return session.size() > 0; + } + + /** + * Method to reset the current countdown index back to 0. + * This method is called when the user wants to stop an ongoing session. + */ + public void resetCurrentCountdownIndex() { + currentCountdownIndex = session.size() - INCREMENT; + primeSessionIsReady(); + } + + public int getWork() { + return work; + } + + public void setWork(int newWork) { + this.work = newWork; + } + + public int getCycle() { + return cycle; + } + + public void setCycle(int newCycles) { + this.cycle = newCycles; + fillSession(); + resetCurrentCountdownIndex(); + } + + public int getBrk() { + return brk; + } + + public void setBrk(int newBrk) { + this.brk = newBrk; + } + + public int getLongBrk() { + return longBrk; + } + + public void setLongBrk(int newLongBrk) { + this.longBrk = newLongBrk; + } + +} diff --git a/src/main/java/wellnus/gamification/GamificationManager.java b/src/main/java/wellnus/gamification/GamificationManager.java new file mode 100644 index 0000000000..c01e40464f --- /dev/null +++ b/src/main/java/wellnus/gamification/GamificationManager.java @@ -0,0 +1,123 @@ +package wellnus.gamification; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.exception.StorageException; +import wellnus.exception.TokenizerException; +import wellnus.exception.WellNusException; +import wellnus.gamification.command.HelpCommand; +import wellnus.gamification.command.HomeCommand; +import wellnus.gamification.command.StatsCommand; +import wellnus.gamification.util.GamificationData; +import wellnus.gamification.util.GamificationStorage; +import wellnus.gamification.util.GamificationUi; +import wellnus.manager.Manager; + +/** + * Manager for the gamification feature. Entry point for this class is the runEventDriver() method. + */ +public class GamificationManager extends Manager { + public static final String FEATURE_NAME = "gamif"; + public static final String FEATURE_HELP_DESCRIPTION = "gamif - Gamification - Gamification gives you the " + + "motivation to continue improving your wellness by rewarding you for your efforts!"; + private static final String CLEAN_DATA_FILE_ERROR_MESSAGE = "Gamification data file may remain corrupted."; + private static final String COMMAND_HELP = "help"; + private static final String COMMAND_HOME = "home"; + private static final String COMMAND_STATS = "stats"; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String UNRECOGNISED_COMMAND_ERROR = "Invalid command issued!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = + "Supported commands in Gamification: " + LINE_SEPARATOR + + "stats command " + StatsCommand.COMMAND_USAGE + LINE_SEPARATOR + + "help command " + HelpCommand.COMMAND_USAGE + LINE_SEPARATOR + + "home command " + HomeCommand.COMMAND_USAGE; + private static final String STATS_USAGE = "stats command " + StatsCommand.COMMAND_USAGE; + private static final String HOME_USAGE = "home command " + HomeCommand.COMMAND_USAGE; + private static final String LOAD_GAMIF_DATA_ERROR_MESSAGE = "Previous gamification data will not be restored."; + private GamificationData gamificationData; + private final GamificationUi gamificationUi; + + /** + * Returns an instance of the GamificationManager. + */ + public GamificationManager() { + this.gamificationUi = new GamificationUi(); + try { + GamificationStorage gamificationStorage = new GamificationStorage(); + this.gamificationData = gamificationStorage.loadData(); + } catch (StorageException loadDataException) { + gamificationUi.printErrorFor(loadDataException, LOAD_GAMIF_DATA_ERROR_MESSAGE); + this.gamificationData = new GamificationData(); + } catch (TokenizerException loadDataException) { + gamificationUi.printErrorFor(loadDataException, LOAD_GAMIF_DATA_ERROR_MESSAGE); + try { + GamificationStorage gamificationStorage = new GamificationStorage(); + gamificationStorage.cleanDataFile(); + } catch (StorageException storageException) { + gamificationUi.printErrorFor(storageException, CLEAN_DATA_FILE_ERROR_MESSAGE); + } + this.gamificationData = new GamificationData(); + } + } + + private Command getCommandFor(String command) throws BadCommandException { + HashMap arguments = commandParser.parseUserInput(command); + String cmdKeyword = commandParser.getMainArgument(command); + switch (cmdKeyword) { + case COMMAND_HELP: + return new HelpCommand(arguments); + case COMMAND_HOME: + return new HomeCommand(arguments); + case COMMAND_STATS: + return new StatsCommand(arguments, gamificationData); + default: + throw new BadCommandException(UNRECOGNISED_COMMAND_ERROR); + } + } + + /** + * Returns the name of this feature. + * + * @return Name of the feature that this Manager handles + */ + @Override + public String getFeatureName() { + return FEATURE_NAME; + } + + public GamificationData getGamificationData() { + return gamificationData; + } + + /** + * runEventDriver is the entry point into GamificationManager.
    + *
    + * It is calls the relevant methods to present the user with the gamification feature's interface + * and manage the user's commands. + */ + @Override + public void runEventDriver() { + GamificationUi.printLogo(); + boolean isExit = false; + while (!isExit) { + try { + String commandString = gamificationUi.getCommand(); + Command command = getCommandFor(commandString); + command.execute(); + isExit = HomeCommand.isHome(command); + } catch (WellNusException exception) { + String errorMessage = exception.getMessage(); + if (errorMessage.contains(COMMAND_STATS)) { + gamificationUi.printErrorFor(exception, STATS_USAGE); + } else if (errorMessage.contains(COMMAND_HOME)) { + gamificationUi.printErrorFor(exception, HOME_USAGE); + } else { + gamificationUi.printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + } + } + } + } +} + diff --git a/src/main/java/wellnus/gamification/command/HelpCommand.java b/src/main/java/wellnus/gamification/command/HelpCommand.java new file mode 100644 index 0000000000..a7119dfdb8 --- /dev/null +++ b/src/main/java/wellnus/gamification/command/HelpCommand.java @@ -0,0 +1,180 @@ +package wellnus.gamification.command; + +import java.util.ArrayList; +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.gamification.GamificationManager; +import wellnus.gamification.util.GamificationUi; + +/** + * Implementation of Gamification WellNus' help command. Explains to the user what commands are supported + * by Gamification Feature and how to use each command. + */ +public class HelpCommand extends Command { + public static final String COMMAND_DESCRIPTION = "help - Get help on what commands can be used " + + "in WellNUS++ Gamification Feature"; + public static final String COMMAND_USAGE = "usage: help [command-to-check]"; + private static final String BAD_ARGUMENTS_MESSAGE = "Invalid arguments given to 'help'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'help'!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "help command " + COMMAND_USAGE; + private static final String COMMAND_KEYWORD = "help"; + private static final String NO_FEATURE_KEYWORD = ""; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String HELP_PREAMBLE = "Input `help` to see all available commands." + LINE_SEPARATOR + + "Input `help [command-to-check]` to get usage help for a specific command." + LINE_SEPARATOR + + "Here are all the commands available for you!"; + private static final String PADDING = " "; + private static final String DOT = "."; + private static final int ONE_OFFSET = 1; + private static final int EXPECTED_PAYLOAD_SIZE = 1; + private final GamificationUi gamificationUi; + + /** + * Initialises a HelpCommand Object using the command arguments issued by the user. + * + * @param arguments Command arguments issued by the user + */ + public HelpCommand(HashMap arguments) { + super(arguments); + this.gamificationUi = new GamificationUi(); + } + + private ArrayList getCommandDescriptions() { + ArrayList commandDescriptions = new ArrayList<>(); + commandDescriptions.add(HelpCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(HomeCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(StatsCommand.COMMAND_DESCRIPTION); + return commandDescriptions; + } + + /** + * Prints either the general help message or the command-specific help message + * based on the presence of a payload. + */ + private void printHelpMessage() { + HashMap argumentPayload = getArguments(); + String commandToSearch = argumentPayload.get(COMMAND_KEYWORD).trim().toLowerCase(); + if (commandToSearch.equals(NO_FEATURE_KEYWORD)) { + printGeneralHelpMessage(); + return; + } + printSpecificHelpMessage(commandToSearch); + } + + /** + * Lists all features available in Atomic Habit WellNUS++ and a short description. + */ + public void printGeneralHelpMessage() { + ArrayList commandDescriptions = getCommandDescriptions(); + String outputMessage = GamificationManager.FEATURE_HELP_DESCRIPTION; + outputMessage = outputMessage.concat(System.lineSeparator()); + outputMessage = outputMessage.concat(HELP_PREAMBLE); + outputMessage = outputMessage.concat(System.lineSeparator() + System.lineSeparator()); + + for (int i = 0; i < commandDescriptions.size(); i += 1) { + outputMessage = outputMessage.concat(i + ONE_OFFSET + DOT + PADDING); + outputMessage = outputMessage.concat(commandDescriptions.get(i) + System.lineSeparator()); + } + gamificationUi.printOutputMessage(outputMessage); + } + + /** + * Prints the help message for a given commandToSearch.
    + * If the commandToSearch does not exist, help will print an unknown command + * error message. + */ + public void printSpecificHelpMessage(String commandToSearch) { + switch (commandToSearch) { + case HelpCommand.COMMAND_KEYWORD: + printUsageMessage(HelpCommand.COMMAND_DESCRIPTION, HelpCommand.COMMAND_USAGE); + break; + case HomeCommand.COMMAND_KEYWORD: + printUsageMessage(HomeCommand.COMMAND_DESCRIPTION, HomeCommand.COMMAND_USAGE); + break; + case StatsCommand.COMMAND_KEYWORD: + printUsageMessage(StatsCommand.COMMAND_DESCRIPTION, StatsCommand.COMMAND_USAGE); + break; + default: + BadCommandException unknownCommand = new BadCommandException(COMMAND_INVALID_PAYLOAD); + gamificationUi.printErrorFor(unknownCommand, COMMAND_INVALID_COMMAND_NOTE); + } + } + + private void printUsageMessage(String commandDescription, String usageString) { + String message = commandDescription + System.lineSeparator() + usageString; + gamificationUi.printOutputMessage(message); + } + + @Override + protected String getCommandKeyword() { + return HelpCommand.COMMAND_KEYWORD; + } + + @Override + protected String getFeatureKeyword() { + return HelpCommand.NO_FEATURE_KEYWORD; + } + + /** + * Executes the issued help command.
    + *

    + * Prints a brief description of all Gamification WellNus' supported commands if + * the basic 'help' command was issued.
    + *

    + * Prints a detailed description of a specific feature if the specialised + * 'help' command was issued. + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException exception) { + gamificationUi.printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + return; + } + this.printHelpMessage(); + } + + /** + * Checks whether the given arguments are valid for our help command. + * + * @param arguments Argument-Payload map generated by CommandParser using user's command + * @throws BadCommandException If the command is invalid + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(COMMAND_KEYWORD) : "HelpCommand's payload map does not contain 'help'!"; + // Check if user put in unnecessary payload or arguments + if (arguments.size() > EXPECTED_PAYLOAD_SIZE) { + throw new BadCommandException(BAD_ARGUMENTS_MESSAGE); + } + } + + /** + * Abstract method to ensure developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/gamification/command/HomeCommand.java b/src/main/java/wellnus/gamification/command/HomeCommand.java new file mode 100644 index 0000000000..62d277d421 --- /dev/null +++ b/src/main/java/wellnus/gamification/command/HomeCommand.java @@ -0,0 +1,106 @@ +package wellnus.gamification.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; +import wellnus.gamification.util.GamificationUi; + +/** + * Provides the 'home' command for the gamification feature. + */ +public class HomeCommand extends Command { + public static final String COMMAND_DESCRIPTION = "home - Returns the user to the main WellNus++ session"; + public static final String COMMAND_KEYWORD = "home"; + public static final String COMMAND_USAGE = "usage: home"; + public static final String FEATURE_NAME = "gamif"; + private static final int NUM_OF_ARGUMENTS = 1; + private static final String WRONG_COMMAND_KEYWORD_MESSAGE = "Invalid command issued, expected 'home'!"; + private static final String WRONG_ARGUMENTS_MESSAGE = "Invalid arguments given to 'home'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'home'!"; + + /** + * Initialises a Command Object to handle the 'home' command from the user + * + * @param arguments Arguments issued by the user + */ + public HomeCommand(HashMap arguments) { + super(arguments); + } + + public static boolean isHome(Command command) { + return command instanceof HomeCommand; + } + + /** + * Returns the home command's activation keyword. + * + * @return String Keyword of the home command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Returns the keyword for the gamification feature. + * + * @return String Keyword for the gamification feature + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Executes the 'home' command from the user to return to the main WellNus feature.
    + *

    + * May throw Exceptions if command fails. + * + * @throws WellNusException If the home command fails + */ + @Override + public void execute() throws WellNusException { + validateCommand(getArguments()); + GamificationUi.printGoodbye(); + } + + /** + * Validate the arguments given by the user.
    + * + * @param arguments Arguments given by the user + * @throws BadCommandException If the arguments have any issues + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(getCommandKeyword()) : WRONG_COMMAND_KEYWORD_MESSAGE; + if (arguments.size() > NUM_OF_ARGUMENTS) { + throw new BadCommandException(WRONG_ARGUMENTS_MESSAGE); + } + String payload = arguments.get(getCommandKeyword()); + if (!payload.isBlank()) { + throw new BadCommandException(COMMAND_INVALID_PAYLOAD); + } + } + + /** + * Returns a description of the home command's syntax. + * + * @return String of the home command's syntax + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Returns a description of the home command's description. + * + * @return String of the description of this home command + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/gamification/command/StatsCommand.java b/src/main/java/wellnus/gamification/command/StatsCommand.java new file mode 100644 index 0000000000..8cc61cfed0 --- /dev/null +++ b/src/main/java/wellnus/gamification/command/StatsCommand.java @@ -0,0 +1,104 @@ +package wellnus.gamification.command; + +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; +import wellnus.gamification.util.GamificationData; +import wellnus.gamification.util.GamificationUi; + +/** + * Provides the 'stats' command for the gamification feature that displays the user's XP statistics, such + * as current XP level. + */ +public class StatsCommand extends Command { + public static final String COMMAND_DESCRIPTION = "stats - Displays the user's XP level and points"; + public static final String COMMAND_KEYWORD = "stats"; + public static final String COMMAND_USAGE = "usage: stats"; + public static final String FEATURE_NAME = "gamif"; + private static final String INVALID_PAYLOAD = "Invalid payload given to 'stats'!"; + private static final int NUM_OF_ARGUMENTS = 1; + private static final String WRONG_ARGUMENTS_MESSAGE = "Invalid arguments given to 'stats'!"; + private static final String WRONG_COMMAND_MESSAGE = "Invalid command issued, expected 'stats'!"; + private final GamificationData gamData; + + /** + * Returns an instance of the StatsCommand Object to handle the 'stats' command from the user + * + * @param arguments Arguments given by the user + */ + public StatsCommand(HashMap arguments, GamificationData gamData) { + super(arguments); + this.gamData = gamData; + } + + /** + * Returns the keyword for the stats command. + * + * @return String Keyword of the stats command + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Returns the keyword for the gamification feature. + * + * @return String Keyword for the gamification feature + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Prints the user's XP statistics on the user's screen. + * @throws WellNusException If XP statistics cannot be successfully printed + * @see GamificationUi#printXpBar(GamificationData, boolean) + */ + @Override + public void execute() throws WellNusException { + validateCommand(getArguments()); + GamificationUi.printXpBar(gamData, true); + } + + /** + * Validates the arguments given for the stats command. + * + * @param arguments Arguments given by the user + * @throws BadCommandException If the arguments are invalid for this stats command + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(getCommandKeyword()) : WRONG_COMMAND_MESSAGE; + if (arguments.size() > NUM_OF_ARGUMENTS) { + throw new BadCommandException(WRONG_ARGUMENTS_MESSAGE); + } + String payload = arguments.get(getCommandKeyword()); + if (!payload.isBlank()) { + throw new BadCommandException(INVALID_PAYLOAD); + } + } + + /** + * Returns the syntax for this stats command. + * + * @return String of the stats command's syntax + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Returns a description of this stats command. + * + * @return String of the description of this stats command + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/gamification/util/GamificationData.java b/src/main/java/wellnus/gamification/util/GamificationData.java new file mode 100644 index 0000000000..d34e811934 --- /dev/null +++ b/src/main/java/wellnus/gamification/util/GamificationData.java @@ -0,0 +1,107 @@ +package wellnus.gamification.util; + +import wellnus.exception.StorageException; + +/** + * Data structure for encapsulating WellNus++ gamification data such as experience + * points and experience levels. See public methods to understand how to update the + * gamification data. + */ +public class GamificationData { + protected static final int POINTS_PER_LEVEL = 10; + private static final int INITIAL_XP_POINTS = 0; + private static final String INVALID_EXP_POINTS_TO_ADD_ERROR = "Cannot add non-positive amount of " + + "experience points: '%d'"; + private static final String INVALID_EXP_POINTS_TO_MINUS_ERROR = "Cannot minus non-positive amount of " + + "experience points: '%d'"; + // Experience points accumulated so far + private int xp; + // Experience level based on the experience points + private int level; + + /** + * Returns an instance of the GamificationData class. + */ + public GamificationData() { + this(INITIAL_XP_POINTS); + } + + /** + * Returns an instance of the GamificationData class with the given amount of XP. + * @param xp Amount of XP to start with + */ + public GamificationData(int xp) { + this.xp = xp; + this.level = getLevelFor(xp); + } + + private static int getLevelFor(int xp) { + return xp / POINTS_PER_LEVEL; + } + + /** + * Increases the user's XP points by the given amount. + * @param pointsToAdd Number of XP points to increase + * @return Whether the user just levelled up + * @throws StorageException If latest XP statistics cannot be saved to storage successfully + */ + public boolean addXp(int pointsToAdd) throws StorageException { + assert pointsToAdd > 0 : String.format(INVALID_EXP_POINTS_TO_ADD_ERROR, pointsToAdd); + xp += pointsToAdd; + int newLevel = getLevelFor(xp); + boolean hasLevelledUp = newLevel > level; + level = newLevel; + GamificationStorage gamificationStorage = new GamificationStorage(); + gamificationStorage.store(this); + return hasLevelledUp; + } + + /** + * Returns the XP collected in the user's current level. + * @return Amount of XP points collected in the current level + */ + public int getXpForCurrentLevelOnly() { + return getTotalXp() - (getXpLevel() * POINTS_PER_LEVEL); + } + + /** + * Returns the total number of XP points user has collected in WellNUS++. + * @return Total number of XP points for the current user + */ + public int getTotalXp() { + return xp; + } + + /** + * Returns the user's current XP level + * @return User's current XP level + */ + public int getXpLevel() { + return level; + } + + /** + * Returns the number of XP points required to reach the next level. + * @return Number of XP points required to level up + */ + public int getXpToReachNextLevel() { + return POINTS_PER_LEVEL - getXpForCurrentLevelOnly(); + } + + /** + * Decreases the user's total XP points by the given amount. + * @param pointsToMinus Number of XP points to deduct from the user + * @return Whether the user dropped by one level due to the XP deduction + * @throws StorageException If latest XP statistics cannot be saved to storage successfully + */ + public boolean minusXp(int pointsToMinus) throws StorageException { + assert pointsToMinus > 0 : String.format(INVALID_EXP_POINTS_TO_MINUS_ERROR, pointsToMinus); + xp -= pointsToMinus; + int newLevel = getLevelFor(xp); + boolean hasLevelDropped = newLevel < level; + level = newLevel; + GamificationStorage gamificationStorage = new GamificationStorage(); + gamificationStorage.store(this); + return hasLevelDropped; + } +} diff --git a/src/main/java/wellnus/gamification/util/GamificationStorage.java b/src/main/java/wellnus/gamification/util/GamificationStorage.java new file mode 100644 index 0000000000..862b1d54ae --- /dev/null +++ b/src/main/java/wellnus/gamification/util/GamificationStorage.java @@ -0,0 +1,70 @@ +package wellnus.gamification.util; + +import java.util.ArrayList; + +import wellnus.exception.StorageException; +import wellnus.exception.TokenizerException; +import wellnus.storage.GamificationTokenizer; +import wellnus.storage.Storage; + +/** + * Manages the storage and retrieval of gamification data to and from storage. + */ +public class GamificationStorage { + private static final int EMPTY = 0; + private static final int FIRST_INDEX = 0; + private final Storage storage; + private final GamificationTokenizer tokenizer; + + /** + * Returns an instance of GamificationStorage. + * @throws StorageException If Storage class cannot be initialised successfully + */ + public GamificationStorage() throws StorageException { + this.storage = new Storage(); + this.tokenizer = new GamificationTokenizer(); + } + + /** + * Cleans the gamification data file, in cases such as when the data file is corrupted. + * @throws StorageException If data file cannot be overwritten successfully + */ + public void cleanDataFile() throws StorageException { + String emptyData = ""; + ArrayList emptyDatas = new ArrayList<>(); + emptyDatas.add(emptyData); + storage.saveData(emptyDatas, Storage.FILE_GAMIFICATION); + } + + /** + * Loads tokenized gamification data from storage and detokenizes them back into + * a GamificationData object. + * @return GamificationData object representing the previously saved gamification statistics + * @throws StorageException If data cannot be fetched from storage successfully + * @throws TokenizerException If tokenized data cannot be detokenized into a GamificationData object + */ + public GamificationData loadData() throws StorageException, TokenizerException { + if (storage.checkFileExists(Storage.FILE_GAMIFICATION)) { + ArrayList tokenizedObjects = storage.loadData(Storage.FILE_GAMIFICATION); + ArrayList dataObjects = tokenizer.detokenize(tokenizedObjects); + if (dataObjects.size() == EMPTY) { + return new GamificationData(); + } + return dataObjects.get(FIRST_INDEX); + } + return new GamificationData(); + } + + /** + * Stores the given GamificationData object in local storage. + * GamificationData is first converted into a String before being written to storage. + * @param data GamificationData object representing the current gamification statistics we're saving + * @throws StorageException If gamification statistics cannot be saved in storage successfully + */ + public void store(GamificationData data) throws StorageException { + ArrayList objectsToStore = new ArrayList<>(); + objectsToStore.add(data); + ArrayList tokenizedObjects = tokenizer.tokenize(objectsToStore); + storage.saveData(tokenizedObjects, Storage.FILE_GAMIFICATION); + } +} diff --git a/src/main/java/wellnus/gamification/util/GamificationUi.java b/src/main/java/wellnus/gamification/util/GamificationUi.java new file mode 100644 index 0000000000..bf8af37770 --- /dev/null +++ b/src/main/java/wellnus/gamification/util/GamificationUi.java @@ -0,0 +1,120 @@ +package wellnus.gamification.util; + +import wellnus.gamification.GamificationManager; +import wellnus.ui.TextUi; + +/** + * Provides helper methods for printing to the user's screen with the gamification feature's unique style. + */ +public class GamificationUi extends TextUi { + public static final String SEPARATOR = "#"; + private static final int NUM_CHAR_IN_SEPARATOR = 70; + private static final String CELEBRATE_LEVEL_UP_MESSAGE = "Congratulations! Level up"; + private static final String GOODBYE_MESSAGE = "Thank you for using the gamification feature! Return anytime"; + private static final String LOGO = + " ______ _ _____ __ _ " + System.lineSeparator() + + " / ____/___ _____ ___ (_) __(_)________ _/ /_(_)___ ____ " + System.lineSeparator() + + " / / __/ __ `/ __ `__ \\/ / /_/ / ___/ __ `/ __/ / __ \\/ __ \\" + System.lineSeparator() + + " / /_/ / /_/ / / / / / / / __/ / /__/ /_/ / /_/ / /_/ / / / /" + System.lineSeparator() + + " \\____/\\__,_/_/ /_/ /_/_/_/ /_/\\___/\\__,_/\\__/_/\\____/_/ /_/ "; + private static final int SHIFT_ONE_DECIMAL_DIVISOR = 10; + private static final String WRONG_NUM_CHAR_IN_SEPARATOR_MESSAGE = "Wrong NUM_CHAR_IN_SEPARATOR value in " + + "GamificationUi"; + private static final String XP_BAR_CHAR = "="; + private static final String XP_BAR_HEAD = ">"; + private static final String XP_BOX_LEFT = "["; + private static final String XP_BOX_RIGHT = "]"; + private static final String XP_TILL_NEXT_LVL_MESSAGE = "%d more XP to Level %d"; + + /** + * Returns a new instance of GamificationUi with custom cursorName and separator + * for our unique gamification style. + */ + public GamificationUi() { + super(); + super.setCursorName(GamificationManager.FEATURE_NAME); + super.setSeparator(SEPARATOR); + super.setSeparatorLength(NUM_CHAR_IN_SEPARATOR); + } + + private static void printGamificationSeparator() { + for (int i = 0; i < NUM_CHAR_IN_SEPARATOR; i += 1) { + System.out.print(SEPARATOR); + } + System.out.print(System.lineSeparator()); + } + + /** + * Prints a congratulations message in the case where the user just levelled up. + */ + public static void printCelebrateLevelUp() { + printGamificationSeparator(); + printGamificationMessage(CELEBRATE_LEVEL_UP_MESSAGE); + printGamificationSeparator(); + } + + /** + * Prints a goodbye message when the user exits from the gamification feature. + */ + public static void printGoodbye() { + printGamificationSeparator(); + printGamificationMessage(GOODBYE_MESSAGE); + printGamificationSeparator(); + } + + /** + * Prints the gamification feature's unique logo. + */ + public static void printLogo() { + printGamificationSeparator(); + System.out.println(" Welcome to"); + System.out.println(LOGO); + printGamificationSeparator(); + } + + /** + * Prints the given message with the gamification feature's unique style. + * @param msg Message to display on the user's screen + */ + public static void printGamificationMessage(String msg) { + assert NUM_CHAR_IN_SEPARATOR >= (msg.length() + 2) : WRONG_NUM_CHAR_IN_SEPARATOR_MESSAGE; + System.out.print(SEPARATOR); + int howManySeparator = 2; + int minimalPadding = 1; + // If assertion is not enabled in JVM, we still want to prevent crashing WellNUS++ + int leftPadding = NUM_CHAR_IN_SEPARATOR < (msg.length() + 2) ? minimalPadding + : ((NUM_CHAR_IN_SEPARATOR - msg.length() - howManySeparator) / 2); + System.out.print(" ".repeat(leftPadding)); + System.out.print(msg); + // Likewise, if assertion is not enabled in JVM, we still want to prevent crashing WellNUS++ + int rightPadding = NUM_CHAR_IN_SEPARATOR < (msg.length() + 2) ? minimalPadding + : (NUM_CHAR_IN_SEPARATOR - msg.length() - howManySeparator - leftPadding); + System.out.print(" ".repeat(rightPadding)); + System.out.println(SEPARATOR); + } + + /** + * Prints the statistics for the user's XP points. + * @param gamData GamificationData object containing the user's current XP data + * @param shouldPrintXpRemaining Whether to print the amount of XP remaining before the user levels up + */ + public static void printXpBar(GamificationData gamData, boolean shouldPrintXpRemaining) { + int currentLevelXp = gamData.getXpForCurrentLevelOnly(); + int currentLevelTotalXp = currentLevelXp + gamData.getXpToReachNextLevel(); + int xpLevel = gamData.getXpLevel(); + int howManyXpBarSegments = currentLevelXp * SHIFT_ONE_DECIMAL_DIVISOR / currentLevelTotalXp; + int numSpacesInXpBar = gamData.getXpToReachNextLevel() + 1; + String padding = " ".repeat(numSpacesInXpBar); + String xpBoxBuilder = XP_BOX_LEFT + + XP_BAR_CHAR.repeat(howManyXpBarSegments) + XP_BAR_HEAD + + padding + XP_BOX_RIGHT; + printGamificationSeparator(); + printGamificationMessage(String.format("Current XP: Level %d %s", xpLevel, xpBoxBuilder)); + if (shouldPrintXpRemaining) { + int nextLevel = xpLevel + 1; + printGamificationMessage(String.format(XP_TILL_NEXT_LVL_MESSAGE, + gamData.getXpToReachNextLevel(), nextLevel)); + } + printGamificationSeparator(); + } +} diff --git a/src/main/java/wellnus/manager/Manager.java b/src/main/java/wellnus/manager/Manager.java new file mode 100644 index 0000000000..d1d42b61ad --- /dev/null +++ b/src/main/java/wellnus/manager/Manager.java @@ -0,0 +1,70 @@ +package wellnus.manager; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; + +//@@author nichyjt + +/** + * Manager is the superclass for all WellNUS++ event drivers.
    + *
    + * Each manager is in charge of 'managing' exactly one feature.
    + * For example, hb and reflect.
    + *
    + *

    + * Each feature consists of multiple MainCommands, + * stored in supportedCommands
    + *
    + *

    + * Each manager may also support entering other features + * via Manager (event drivers), + * stored in supportedManagers
    + *
    + *

    + * The manager should run an event driver (infinite loop) and is in charge + * of a Feature's input, output, 'business' logic and graceful termination. + */ +public abstract class Manager { + + protected CommandParser commandParser; + + /** + * Construct a feature Manager to handle control flow for the given feature.
    + *
    + * Internally, it sets up the following for convenience: + *

  • CommandParser
  • + *
  • Supported Commands
  • + */ + public Manager() { + this.commandParser = new CommandParser(); + } + + /** + * Utility function to get the CommandParser tied to the Manager class. + * + * @return CommandParser reference to this manager's instance of CommandParser + */ + public CommandParser getCommandParser() { + assert commandParser != null : "commandParser should not be null"; + return this.commandParser; + } + + /** + * Utility function to get the featureName this Manager is administering. + * + * @return Name of the feature that this Manager handles + */ + public abstract String getFeatureName(); + + /** + * runEventDriver is the entry point into a feature's driver loop.
    + *
    + * This should be the part that contains the infinite loop and switch cases, + * but it is up to the implementer.
    + * Its implementation should include the following: + *
  • A way to terminate runEventDriver
  • + *
  • A way to read input from console
  • + */ + public abstract void runEventDriver() throws BadCommandException; + +} diff --git a/src/main/java/wellnus/reflection/command/FavoriteCommand.java b/src/main/java/wellnus/reflection/command/FavoriteCommand.java new file mode 100644 index 0000000000..0d35b9bf66 --- /dev/null +++ b/src/main/java/wellnus/reflection/command/FavoriteCommand.java @@ -0,0 +1,146 @@ +package wellnus.reflection.command; + +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectUi; + +//@@author wenxin-c +/** + * Get all the questions that are in the favorite list. + */ +public class FavoriteCommand extends Command { + public static final String COMMAND_DESCRIPTION = "fav - Get the list of questions that have been added to " + + "the favorite list."; + public static final String COMMAND_USAGE = "usage: fav"; + public static final String COMMAND_KEYWORD = "fav"; + private static final String PAYLOAD = ""; + private static final String FEATURE_NAME = "reflect"; + private static final String INVALID_COMMAND_MSG = "Invalid command issued, expected 'fav'!"; + private static final String INVALID_ARGUMENTS_MSG = "Invalid arguments given to 'fav'!"; + private static final String INVALID_PAYLOAD = "Invalid payload given to 'fav'!"; + private static final String INVALID_COMMAND_NOTES = "fav command " + COMMAND_USAGE; + private static final String COMMAND_KEYWORD_ASSERTION = "The key should be fav."; + private static final String COMMAND_PAYLOAD_ASSERTION = "The payload should be empty."; + private static final String INDEX_OUT_OF_BOUND_MSG = "Invalid index given, index is out of bound!"; + private static final String EMPTY_FAV_LIST = "There is nothing in favorite list, " + + "please get reflection questions first!"; + private static final int ARGUMENT_PAYLOAD_SIZE = 1; + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectFavCommandLogger"); + private static final ReflectUi UI = new ReflectUi(); + private QuestionList questionList; + + /** + * Set up the argument-payload pairs for this command.
    + * Pass in a questionList object from ReflectionManager to access the list of favorite questions. + * + * @param arguments Argument-payload pairs from users + * @param questionList Object that contains the data about questions + */ + public FavoriteCommand(HashMap arguments, QuestionList questionList) { + super(arguments); + this.questionList = questionList; + } + + /** + * Get the command itself. + * + * @return Command: get + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Get the name of the feature in which this fav command is generated. + * + * @return Feature name: reflect + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + + /** + * Entry point to this command.
    + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException invalidCommand) { + LOGGER.log(Level.INFO, INVALID_COMMAND_MSG); + UI.printErrorFor(invalidCommand, INVALID_COMMAND_NOTES); + return; + } + if (!questionList.hasFavQuestions()) { + UI.printOutputMessage(EMPTY_FAV_LIST); + return; + } + try { + String outputString = questionList.getFavQuestions(); + UI.printOutputMessage(outputString); + } catch (IndexOutOfBoundsException indexOutOfBoundsException) { + LOGGER.log(Level.WARNING, INDEX_OUT_OF_BOUND_MSG); + UI.printErrorFor(indexOutOfBoundsException, INVALID_COMMAND_NOTES); + } + } + + /** + * Validate the command.
    + *
    + * Conditions for command to be valid:
    + *

  • Only one argument-payload pair + *
  • The pair contains key: fav + *
  • Payload is empty + * Whichever mismatch will cause the command to be invalid. + * + * @param commandMap Argument-Payload map generated by CommandParser + * @throws BadCommandException If an invalid command is given + */ + @Override + public void validateCommand(HashMap commandMap) throws BadCommandException { + if (commandMap.size() != ARGUMENT_PAYLOAD_SIZE) { + throw new BadCommandException(INVALID_ARGUMENTS_MSG); + } else if (!commandMap.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(INVALID_COMMAND_MSG); + } else if (!commandMap.get(COMMAND_KEYWORD).equals(PAYLOAD)) { + throw new BadCommandException(INVALID_PAYLOAD); + } + assert getArguments().containsKey(COMMAND_KEYWORD) : COMMAND_KEYWORD_ASSERTION; + assert getArguments().get(COMMAND_KEYWORD).equals(PAYLOAD) : COMMAND_PAYLOAD_ASSERTION; + } +} + diff --git a/src/main/java/wellnus/reflection/command/GetCommand.java b/src/main/java/wellnus/reflection/command/GetCommand.java new file mode 100644 index 0000000000..9e37351d07 --- /dev/null +++ b/src/main/java/wellnus/reflection/command/GetCommand.java @@ -0,0 +1,185 @@ +package wellnus.reflection.command; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.exception.StorageException; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectUi; +import wellnus.reflection.feature.ReflectionQuestion; + +//@@author wenxin-c +/** + * Command to get a set of 5 random questions. + */ +public class GetCommand extends Command { + public static final String COMMAND_DESCRIPTION = "get - Get a list of questions to reflect on."; + public static final String COMMAND_USAGE = "usage: get"; + public static final String COMMAND_KEYWORD = "get"; + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectGetCommandLogger"); + private static final String FEATURE_NAME = "reflect"; + private static final String PAYLOAD = ""; + private static final String INVALID_COMMAND_MSG = "Invalid command issued, expected 'get'!"; + private static final String INVALID_ARGUMENT_MSG = "Invalid arguments given to 'get'!"; + private static final String INVALID_PAYLOAD = "Invalid payload given to 'get'!"; + private static final String INVALID_COMMAND_NOTES = "get command " + COMMAND_USAGE; + private static final String COMMAND_KEYWORD_ASSERTION = "The key should be get."; + private static final String COMMAND_PAYLOAD_ASSERTION = "The payload should be empty."; + private static final String NUM_SELECTED_QUESTIONS_ASSERTION = "The number of selected questions should be 5."; + private static final String STORAGE_ERROR = "Error saving to storage!"; + private static final String DOT = "."; + private static final String EMPTY_STRING = ""; + private static final int NUM_OF_RANDOM_QUESTIONS = 5; + private static final int ARGUMENT_PAYLOAD_SIZE = 1; + private static final int ONE_OFFSET = 1; + private static final ReflectUi UI = new ReflectUi(); + private Set randomQuestionIndexes; + private QuestionList questionList; + + /** + * Set up the argument-payload pairs for this command.
    + * Pass in a questionList object from ReflectionManager to access the list of questions. + * + * @param arguments Argument-payload pairs from users + * @param questionList Object that contains the data about questions + */ + public GetCommand(HashMap arguments, QuestionList questionList) { + super(arguments); + this.questionList = questionList; + } + + /** + * Get the command itself. + * + * @return Command: get + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Get the name of the feature in which this get command is generated. + * + * @return Feature name: reflect + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + + /** + * Entry point to this command.
    + * Trigger the generation of five random questions and print to users.
    + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException invalidCommand) { + LOGGER.log(Level.INFO, INVALID_COMMAND_MSG); + UI.printErrorFor(invalidCommand, INVALID_COMMAND_NOTES); + return; + } + try { + String outputString = convertQuestionsToString(); + UI.printOutputMessage(outputString); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + UI.printErrorFor(storageException, STORAGE_ERROR); + } + } + + /** + * Validate the command.
    + *
    + * Conditions for command to be valid:
    + *

  • Only one argument-payload pair + *
  • The pair contains key: get + *
  • Payload is empty + * Whichever mismatch will cause the command to be invalid. + * + * @param commandMap Argument-Payload map generated by CommandParser + * @throws BadCommandException If an invalid command is given + */ + @Override + public void validateCommand(HashMap commandMap) throws BadCommandException { + if (commandMap.size() != ARGUMENT_PAYLOAD_SIZE) { + throw new BadCommandException(INVALID_ARGUMENT_MSG); + } else if (!commandMap.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(INVALID_COMMAND_MSG); + } else if (!commandMap.get(COMMAND_KEYWORD).equals(PAYLOAD)) { + throw new BadCommandException(INVALID_PAYLOAD); + } + assert getArguments().containsKey(COMMAND_KEYWORD) : COMMAND_KEYWORD_ASSERTION; + assert getArguments().get(COMMAND_KEYWORD).equals(PAYLOAD) : COMMAND_PAYLOAD_ASSERTION; + } + + /** + * Use questionList object to generate a set of 5 random integers(0-9) which will then be used as indexes to get + * a set of 5 random questions. + *
    + * Each number num: num >= 0 and num <= (maxSize - 1) + * + * @return The selected sets of random questions + */ + public ArrayList getRandomQuestions() throws StorageException { + questionList.setRandomQuestionIndexes(); + this.randomQuestionIndexes = questionList.getRandomQuestionIndexes(); + ArrayList selectedQuestions = new ArrayList<>(); + ArrayList questions = questionList.getAllQuestions(); + for (int index : this.randomQuestionIndexes) { + selectedQuestions.add(questions.get(index)); + } + assert selectedQuestions.size() == NUM_OF_RANDOM_QUESTIONS : NUM_SELECTED_QUESTIONS_ASSERTION; + return selectedQuestions; + } + + /** + * Convert all five questions to a single string to be printed. + * + * @return Single string that consists of all questions + */ + private String convertQuestionsToString() throws StorageException { + ArrayList selectedQuestions = getRandomQuestions(); + String questionString = EMPTY_STRING; + for (int i = 0; i < selectedQuestions.size(); i += 1) { + questionString += ((i + ONE_OFFSET) + DOT + selectedQuestions.get(i).toString() + + System.lineSeparator()); + } + return questionString; + } +} + diff --git a/src/main/java/wellnus/reflection/command/HelpCommand.java b/src/main/java/wellnus/reflection/command/HelpCommand.java new file mode 100644 index 0000000000..33408d70e1 --- /dev/null +++ b/src/main/java/wellnus/reflection/command/HelpCommand.java @@ -0,0 +1,202 @@ +package wellnus.reflection.command; + +import java.util.ArrayList; +import java.util.HashMap; + +import wellnus.command.Command; +import wellnus.exception.BadCommandException; +import wellnus.reflection.feature.ReflectUi; +import wellnus.reflection.feature.ReflectionManager; +import wellnus.ui.TextUi; + +/** + * Implementation of Reflection WellNus' help command. Explains to the user what commands are supported + * by Reflection and how to use each command. + */ + +public class HelpCommand extends Command { + public static final String COMMAND_DESCRIPTION = "help - Get help on what commands can be used " + + "in Reflection WellNUS++"; + public static final String COMMAND_USAGE = "usage: help [command-to-check]"; + private static final String BAD_ARGUMENTS_MESSAGE = "Invalid arguments given to 'help'!"; + private static final String COMMAND_INVALID_PAYLOAD = "Invalid payload given to 'help'!"; + private static final String COMMAND_INVALID_COMMAND_NOTE = "help command " + COMMAND_USAGE; + private static final String COMMAND_KEYWORD = "help"; + private static final String NO_FEATURE_KEYWORD = ""; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String HELP_PREAMBLE = "Input `help` to see all available commands." + LINE_SEPARATOR + + "Input `help [command-to-check] to get usage help for a specific command." + LINE_SEPARATOR + + "Here are all the commands available for you!"; + private static final String PADDING = " "; + private static final String DOT = "."; + private static final int ONE_OFFSET = 1; + private static final int EXPECTED_PAYLOAD_SIZE = 1; + private final ReflectUi reflectUi; + + /** + * Initialises a HelpCommand Object using the command arguments issued by the user. + * + * @param arguments Command arguments issued by the user + */ + public HelpCommand(HashMap arguments) { + super(arguments); + this.reflectUi = new ReflectUi(); + } + + private TextUi getTextUi() { + return this.reflectUi; + } + + private ArrayList getCommandDescriptions() { + ArrayList commandDescriptions = new ArrayList<>(); + commandDescriptions.add(FavoriteCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(GetCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(HelpCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(HomeCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(LikeCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(UnlikeCommand.COMMAND_DESCRIPTION); + commandDescriptions.add(PrevCommand.COMMAND_DESCRIPTION); + return commandDescriptions; + } + + /** + * Prints either the general help message or the command-specific help message + * based on the presence of a payload. + */ + private void printHelpMessage() { + HashMap argumentPayload = getArguments(); + String commandToSearch = argumentPayload.get(COMMAND_KEYWORD).trim().toLowerCase(); + if (commandToSearch.equals(NO_FEATURE_KEYWORD)) { + printGeneralHelpMessage(); + return; + } + printSpecificHelpMessage(commandToSearch); + } + + /** + * Lists all features available in Atomic Habit WellNUS++ and a short description. + */ + public void printGeneralHelpMessage() { + ArrayList commandDescriptions = getCommandDescriptions(); + String outputMessage = ReflectionManager.FEATURE_HELP_DESCRIPTION; + outputMessage = outputMessage.concat(System.lineSeparator()); + outputMessage = outputMessage.concat(HELP_PREAMBLE); + outputMessage = outputMessage.concat(System.lineSeparator() + System.lineSeparator()); + + for (int i = 0; i < commandDescriptions.size(); i += 1) { + outputMessage = outputMessage.concat(i + ONE_OFFSET + DOT + PADDING); + outputMessage = outputMessage.concat(commandDescriptions.get(i) + System.lineSeparator()); + } + this.getTextUi().printOutputMessage(outputMessage); + } + + /** + * Prints the help message for a given commandToSearch.
    + * If the commandToSearch does not exist, help will print an unknown command + * error message. + */ + public void printSpecificHelpMessage(String commandToSearch) { + switch (commandToSearch) { + case FavoriteCommand.COMMAND_KEYWORD: + printUsageMessage(FavoriteCommand.COMMAND_DESCRIPTION, FavoriteCommand.COMMAND_USAGE); + break; + case GetCommand.COMMAND_KEYWORD: + printUsageMessage(GetCommand.COMMAND_DESCRIPTION, GetCommand.COMMAND_USAGE); + break; + case HelpCommand.COMMAND_KEYWORD: + printUsageMessage(HelpCommand.COMMAND_DESCRIPTION, HelpCommand.COMMAND_USAGE); + break; + case HomeCommand.COMMAND_KEYWORD: + printUsageMessage(HomeCommand.COMMAND_DESCRIPTION, HomeCommand.COMMAND_USAGE); + break; + case LikeCommand.COMMAND_KEYWORD: + printUsageMessage(LikeCommand.COMMAND_DESCRIPTION, LikeCommand.COMMAND_USAGE); + break; + case UnlikeCommand.COMMAND_KEYWORD: + printUsageMessage(UnlikeCommand.COMMAND_DESCRIPTION, UnlikeCommand.COMMAND_USAGE); + break; + case PrevCommand.COMMAND_KEYWORD: + printUsageMessage(PrevCommand.COMMAND_DESCRIPTION, PrevCommand.COMMAND_USAGE); + break; + default: + BadCommandException unknownCommand = new BadCommandException(COMMAND_INVALID_PAYLOAD); + reflectUi.printErrorFor(unknownCommand, COMMAND_INVALID_COMMAND_NOTE); + } + } + + private void printUsageMessage(String commandDescription, String usageString) { + String message = commandDescription + System.lineSeparator() + usageString; + reflectUi.printOutputMessage(message); + } + + @Override + protected String getCommandKeyword() { + return HelpCommand.COMMAND_KEYWORD; + } + + @Override + protected String getFeatureKeyword() { + return HelpCommand.NO_FEATURE_KEYWORD; + } + + /** + * Executes the issued help command.
    + *

    + * Prints a brief description of all Reflection WellNus' supported commands if + * the basic 'help' command was issued.
    + *

    + * Prints a detailed description of a specific feature if the specialised + * 'help' command was issued. + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException exception) { + getTextUi().printErrorFor(exception, COMMAND_INVALID_COMMAND_NOTE); + return; + } + this.printHelpMessage(); + } + + /** + * Checks whether the given arguments are valid for our help command. + * + * @param arguments Argument-Payload map generated by CommandParser using user's command + * @throws BadCommandException If the command is invalid + */ + @Override + public void validateCommand(HashMap arguments) throws BadCommandException { + assert arguments.containsKey(COMMAND_KEYWORD) : "HelpCommand's payload map does not contain 'help'!"; + // Check if user put in unnecessary payload or arguments + if (arguments.size() > EXPECTED_PAYLOAD_SIZE) { + throw new BadCommandException(BAD_ARGUMENTS_MESSAGE); + } + } + + /** + * Abstract method to ensure developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/wellnus/reflection/command/HomeCommand.java b/src/main/java/wellnus/reflection/command/HomeCommand.java new file mode 100644 index 0000000000..1ee809bbb7 --- /dev/null +++ b/src/main/java/wellnus/reflection/command/HomeCommand.java @@ -0,0 +1,140 @@ +package wellnus.reflection.command; + +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectUi; +import wellnus.reflection.feature.ReflectionManager; + +//@@author wenxin-c +/** + * Home command to return back to WellNUS++ main interface. + */ +public class HomeCommand extends Command { + public static final String COMMAND_DESCRIPTION = "home - Return back to the main menu of WellNUS++."; + public static final String COMMAND_USAGE = "usage: home"; + public static final String COMMAND_KEYWORD = "home"; + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectHomeCommandLogger"); + private static final String FEATURE_NAME = "reflect"; + private static final String PAYLOAD = ""; + private static final String INVALID_COMMAND_MSG = "Invalid command issued, expected 'home'!"; + private static final String INVALID_ARGUMENT_MSG = "Invalid arguments given to 'home'!"; + private static final String INVALID_PAYLOAD = "Invalid payload given to 'home'!"; + private static final String INVALID_COMMAND_NOTES = "home command " + COMMAND_USAGE; + private static final String COMMAND_PAYLOAD_ASSERTION = "The payload should be empty."; + private static final String HOME_MESSAGE = "How do you feel after reflecting on yourself?" + + System.lineSeparator() + "Hope you have gotten some takeaways from self reflection, see you again!!"; + private static final int ARGUMENT_PAYLOAD_SIZE = 1; + private static final ReflectUi UI = new ReflectUi(); + private QuestionList questionList; + + /** + * Set up the argument-payload pairs for this command.
    + * Pass in a questionList object from ReflectionManager to manipulate history data. + * + * @param arguments Argument-payload pairs from users + * @param questionList Object that contains the data about questions + */ + public HomeCommand(HashMap arguments, QuestionList questionList) { + super(arguments); + this.questionList = questionList; + } + + /** + * Get the command itself. + * + * @return Command: home + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Get the name of the feature in which this home command is generated. + * + * @return Feature name: reflect + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + + /** + * Main entry point of this command.
    + * Return back to WellNUS++ main interface and clear the questionList history data. + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException invalidCommand) { + LOGGER.log(Level.INFO, INVALID_COMMAND_MSG); + UI.printErrorFor(invalidCommand, INVALID_COMMAND_NOTES); + return; + } + UI.printOutputMessage(HOME_MESSAGE); + if (!questionList.getRandomQuestionIndexes().isEmpty()) { + questionList.clearRandomQuestionIndexes(); + } + ReflectionManager.setIsExit(true); + } + + /** + * Validate the command.
    + *
    + * Conditions for command to be valid:
    + *

  • Only one argument-payload pair + *
  • The pair contains key: home + *
  • Payload is empty + * Whichever mismatch will cause the command to be invalid. + * + * @param commandMap Argument-Payload map generated by CommandParser + * @throws BadCommandException If an invalid command is given + */ + @Override + public void validateCommand(HashMap commandMap) throws BadCommandException { + if (commandMap.size() != ARGUMENT_PAYLOAD_SIZE) { + throw new BadCommandException(INVALID_ARGUMENT_MSG); + } else if (!commandMap.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(INVALID_COMMAND_MSG); + } + String payload = commandMap.get(getCommandKeyword()); + if (!payload.isBlank()) { + throw new BadCommandException(INVALID_PAYLOAD); + } + assert getArguments().get(COMMAND_KEYWORD).equals(PAYLOAD) : COMMAND_PAYLOAD_ASSERTION; + } +} + diff --git a/src/main/java/wellnus/reflection/command/LikeCommand.java b/src/main/java/wellnus/reflection/command/LikeCommand.java new file mode 100644 index 0000000000..96354ba19e --- /dev/null +++ b/src/main/java/wellnus/reflection/command/LikeCommand.java @@ -0,0 +1,184 @@ +package wellnus.reflection.command; + +import java.util.HashMap; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.exception.ReflectionException; +import wellnus.exception.StorageException; +import wellnus.exception.TokenizerException; +import wellnus.reflection.feature.IndexMapper; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectUi; + +//@@author wenxin-c +/** + * Like command to add reflection questions into favorite list. + */ +public class LikeCommand extends Command { + public static final String COMMAND_DESCRIPTION = "like - Add a particular question to favorite list."; + public static final String COMMAND_USAGE = "usage: like (index)"; + public static final String COMMAND_KEYWORD = "like"; + private static final String FEATURE_NAME = "reflect"; + private static final String INVALID_COMMAND_MSG = "Invalid command issued, expected 'like'!"; + private static final String INVALID_ARGUMENT_MSG = "Invalid arguments given to 'like'!"; + private static final String INVALID_COMMAND_NOTES = "like command " + COMMAND_USAGE; + private static final String WRONG_INDEX_MSG = "Invalid index payload given to 'like', expected a valid integer!"; + private static final String WRONG_INDEX_OUT_BOUND = "Invalid index payload given to 'like', index is out of range!"; + private static final String MISSING_SET_QUESTIONS = "A set of questions has not been gotten!" + + System.lineSeparator() + "Please try 'get' command to generate a set of questions " + + "before adding to favorite list!"; + private static final String TOKENIZER_ERROR = "Error tokenizing data!"; + private static final String STORAGE_ERROR = "Error saving to storage!"; + private static final int ARGUMENT_PAYLOAD_SIZE = 1; + private static final int UPPER_BOUND = 5; + private static final int LOWER_BOUND = 1; + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectLikeCommandLogger"); + private static final ReflectUi UI = new ReflectUi(); + private Set randomQuestionIndexes; + private QuestionList questionList; + + /** + * Set up the argument-payload pairs for this command.
    + * Pass in a questionList object from ReflectionManager to access the indexes of the previous set of questions. + * + * @param arguments Argument-payload pairs from users + * @param questionList Object that contains the data about questions + */ + public LikeCommand(HashMap arguments, QuestionList questionList) { + super(arguments); + this.questionList = questionList; + this.randomQuestionIndexes = questionList.getRandomQuestionIndexes(); + } + + /** + * Get the command itself. + * + * @return Command: like + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Get the name of the feature in which this like command is generated. + * + * @return Feature name: reflect + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + + /** + * Validate the command.
    + *
    + * Conditions for command to be valid:
    + *

  • Only one argument-payload pair + *
  • The pair contains key: like + *
  • Payload must be string which parse into integer ranges from 1 to 5 + * Whichever mismatch will cause the command to be invalid. + * + * @param commandMap Argument-Payload map generated by CommandParser + * @throws BadCommandException If an invalid command is given + */ + @Override + public void validateCommand(HashMap commandMap) throws BadCommandException { + if (commandMap.size() != ARGUMENT_PAYLOAD_SIZE) { + throw new BadCommandException(INVALID_ARGUMENT_MSG); + } else if (!commandMap.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(INVALID_COMMAND_MSG); + } + } + + /** + * Entry point to this command.
    + * Check the validity of commands and add into favorite list.
    + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException badCommandException) { + LOGGER.log(Level.INFO, INVALID_COMMAND_MSG); + UI.printErrorFor(badCommandException, INVALID_COMMAND_NOTES); + return; + } + try { + addFavQuestion(getArguments().get(COMMAND_KEYWORD)); + } catch (BadCommandException badCommandException) { + LOGGER.log(Level.INFO, MISSING_SET_QUESTIONS); + UI.printErrorFor(badCommandException, INVALID_COMMAND_NOTES); + } catch (TokenizerException tokenizerException) { + LOGGER.log(Level.WARNING, TOKENIZER_ERROR); + UI.printErrorFor(tokenizerException, TOKENIZER_ERROR); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + UI.printErrorFor(storageException, STORAGE_ERROR); + } catch (NumberFormatException numberFormatException) { + LOGGER.log(Level.INFO, WRONG_INDEX_MSG); + BadCommandException exception = new BadCommandException(WRONG_INDEX_MSG); + UI.printErrorFor(exception, INVALID_COMMAND_NOTES); + } catch (ReflectionException reflectionException) { + UI.printErrorFor(reflectionException, INVALID_COMMAND_NOTES); + } + } + + /** + * Add this index to favorite list and print the question to be added.
    + *
    + * A valid index will only be added(i.e. passed validateCommand()) if there is a set of questions gotten previously + * + * @param questionIndex User input of the index of question to be added to favorite list. + * @throws BadCommandException If there is not a set of question generated yet. + * @throws TokenizerException If there is error in tokenization of index + * @throws StorageException If there is error in storing the data + */ + public void addFavQuestion(String questionIndex) throws BadCommandException, TokenizerException, StorageException, + NumberFormatException, ReflectionException { + int questionIndexInt = Integer.parseInt(questionIndex); + if (questionIndexInt > UPPER_BOUND || questionIndexInt < LOWER_BOUND) { + throw new ReflectionException(WRONG_INDEX_OUT_BOUND); + } + if (!questionList.hasRandomQuestionIndexes()) { + UI.printOutputMessage(MISSING_SET_QUESTIONS); + return; + } + IndexMapper indexMapper = new IndexMapper(this.randomQuestionIndexes); + HashMap indexQuestionMap = indexMapper.mapIndex(); + int indexToAdd = indexQuestionMap.get(questionIndexInt); + questionList.addFavListIndex(indexToAdd); + } +} + diff --git a/src/main/java/wellnus/reflection/command/PrevCommand.java b/src/main/java/wellnus/reflection/command/PrevCommand.java new file mode 100644 index 0000000000..68850062d4 --- /dev/null +++ b/src/main/java/wellnus/reflection/command/PrevCommand.java @@ -0,0 +1,156 @@ +package wellnus.reflection.command; + +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectUi; + +/** + * Get the previous set of question generated for review. + */ +public class PrevCommand extends Command { + public static final String COMMAND_DESCRIPTION = "prev - Get the previously generated set of questions."; + public static final String COMMAND_USAGE = "usage: prev"; + public static final String COMMAND_KEYWORD = "prev"; + private static final String FEATURE_NAME = "reflect"; + private static final String INVALID_COMMAND_MSG = "Invalid command issued, expected 'prev'!"; + private static final String INVALID_ARGUMENT_MSG = "Invalid arguments given to 'prev'!"; + private static final String INVALID_PAYLOAD = "Invalid payload given to 'prev'!"; + private static final String INVALID_COMMAND_NOTES = "prev command " + COMMAND_USAGE; + private static final String COMMAND_KEYWORD_ASSERTION = "The key should be prev."; + private static final String COMMAND_PAYLOAD_ASSERTION = "The payload should be empty."; + private static final String MISSING_SET_QUESTIONS = "A set of questions has not been gotten!" + + System.lineSeparator() + "Please try 'get' command to generate a set of questions " + + "before reviewing them!"; + private static final String INDEX_OUT_OF_BOUND_MSG = "Invalid index payload given to 'prev', " + + "index is out of bound!"; + private static final String PAYLOAD = ""; + private static final int ARGUMENT_PAYLOAD_SIZE = 1; + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectPrevCommandLogger"); + private static final ReflectUi UI = new ReflectUi(); + private QuestionList questionList; + + /** + * Set up the argument-payload pairs for this command.
    + * Pass in a questionList object from ReflectionManager to access the indexes of the previous set of questions. + * + * @param arguments Argument-payload pairs from users + * @param questionList Object that contains the data about questions + */ + public PrevCommand(HashMap arguments, QuestionList questionList) { + super(arguments); + this.questionList = questionList; + } + + /** + * Get the command itself. + * + * @return Command: prev + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Get the name of the feature in which this prev command is generated. + * + * @return Feature name: reflect + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + + /** + * Validate the command.
    + *
    + * Conditions for command to be valid:
    + *

  • Only one argument-payload pair + *
  • The pair contains key: prev + *
  • Payload must be empty + * Whichever mismatch will cause the command to be invalid. + * + * @param commandMap Argument-Payload map generated by CommandParser + * @throws BadCommandException If an invalid command is given + */ + @Override + public void validateCommand(HashMap commandMap) throws BadCommandException { + if (commandMap.size() != ARGUMENT_PAYLOAD_SIZE) { + throw new BadCommandException(INVALID_ARGUMENT_MSG); + } else if (!commandMap.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(INVALID_COMMAND_MSG); + } else if (!commandMap.get(COMMAND_KEYWORD).equals(PAYLOAD)) { + throw new BadCommandException(INVALID_PAYLOAD); + } + assert getArguments().containsKey(COMMAND_KEYWORD) : COMMAND_KEYWORD_ASSERTION; + assert getArguments().get(COMMAND_KEYWORD).equals(PAYLOAD) : COMMAND_PAYLOAD_ASSERTION; + } + + /** + * Entry point to this command.
    + * Check the validity of commands and add into favorite list.
    + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException badCommandException) { + LOGGER.log(Level.INFO, INVALID_COMMAND_MSG); + UI.printErrorFor(badCommandException, INVALID_COMMAND_NOTES); + return; + } + try { + getPrevSetQuestions(); + } catch (IndexOutOfBoundsException indexOutOfBoundsException) { + LOGGER.log(Level.WARNING, INDEX_OUT_OF_BOUND_MSG); + UI.printErrorFor(indexOutOfBoundsException, INVALID_COMMAND_NOTES); + } catch (BadCommandException badCommandException) { + LOGGER.log(Level.WARNING, MISSING_SET_QUESTIONS); + UI.printErrorFor(badCommandException, INVALID_COMMAND_NOTES); + } + } + + /** + * Get and print the previous set of question generated. + */ + public void getPrevSetQuestions() throws BadCommandException { + if (!questionList.hasRandomQuestionIndexes()) { + UI.printOutputMessage(MISSING_SET_QUESTIONS); + return; + } + String prevSetQuestions = this.questionList.getPrevSetQuestions(); + UI.printOutputMessage(prevSetQuestions); + } +} diff --git a/src/main/java/wellnus/reflection/command/UnlikeCommand.java b/src/main/java/wellnus/reflection/command/UnlikeCommand.java new file mode 100644 index 0000000000..dc59bbfc3a --- /dev/null +++ b/src/main/java/wellnus/reflection/command/UnlikeCommand.java @@ -0,0 +1,186 @@ +package wellnus.reflection.command; + +import java.util.HashMap; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.command.Command; +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.exception.ReflectionException; +import wellnus.exception.StorageException; +import wellnus.exception.TokenizerException; +import wellnus.reflection.feature.IndexMapper; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectUi; + +//@@author wenxin-c +/** + * Unlike command to remove reflection questions from favorite questions. + */ +public class UnlikeCommand extends Command { + public static final String COMMAND_DESCRIPTION = "unlike - Remove a particular question " + + "from favorite list."; + public static final String COMMAND_USAGE = "usage: unlike (index)"; + public static final String COMMAND_KEYWORD = "unlike"; + private static final String FEATURE_NAME = "reflect"; + private static final String INVALID_COMMAND_MSG = "Invalid command issued, expected 'unlike'!"; + private static final String INVALID_ARGUMENT_MSG = "Invalid arguments given to 'unlike'!"; + private static final String INVALID_COMMAND_NOTES = "unlike command " + COMMAND_USAGE; + private static final String WRONG_INDEX_MSG = "Invalid index payload given to 'unlike', expected a valid integer!"; + private static final String WRONG_INDEX_OUT_BOUND = "Invalid index payload given to 'unlike', " + + "index is out of range!"; + private static final String EMPTY_FAV_LIST_MSG = "The favorite list is empty, there is nothing to be removed."; + private static final String TOKENIZER_ERROR = "Error tokenizing data!"; + private static final String STORAGE_ERROR = "Error saving to storage!"; + private static final int INDEX_ZERO = 0; + private static final int ARGUMENT_PAYLOAD_SIZE = 1; + private static final int LOWER_BOUND = 1; + private static final int EMPTY_LIST = 0; + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectUnlikeCommandLogger"); + private static final ReflectUi UI = new ReflectUi(); + private Set favQuestionIndexes; + private QuestionList questionList; + + /** + * Set up the argument-payload pairs for this command.
    + * Pass in a questionList object from ReflectionManager to access the indexes of the liked questions. + * + * @param arguments Argument-payload pairs from users + * @param questionList Object that contains the data about questions + */ + public UnlikeCommand(HashMap arguments, QuestionList questionList) { + super(arguments); + this.questionList = questionList; + this.favQuestionIndexes = questionList.getDataIndex().get(INDEX_ZERO); + } + + /** + * Get the command itself. + * + * @return Command: unlike + */ + @Override + protected String getCommandKeyword() { + return COMMAND_KEYWORD; + } + + /** + * Get the name of the feature in which this unlike command is generated. + * + * @return Feature name: reflect + */ + @Override + protected String getFeatureKeyword() { + return FEATURE_NAME; + } + + /** + * Method to ensure that developers add in a command usage. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "usage: add --name (name of habit)" + * + * @return String of the proper usage of the habit + */ + @Override + public String getCommandUsage() { + return COMMAND_USAGE; + } + + /** + * Method to ensure that developers add in a description for the command. + *

    + * For example, for the 'add' command in AtomicHabit package:
    + * "add - add a habit to your list" + * + * @return String of the description of what the command does + */ + @Override + public String getCommandDescription() { + return COMMAND_DESCRIPTION; + } + + /** + * Validate the command.
    + *
    + * Conditions for command to be valid:
    + *

  • Only one argument-payload pair + *
  • The pair contains key: like + *
  • Payload must be string which parse into integer ranges from 1 to 5 + * Whichever mismatch will cause the command to be invalid. + * + * @param commandMap Argument-Payload map generated by CommandParser + * @throws BadCommandException If an invalid command is given + */ + @Override + public void validateCommand(HashMap commandMap) throws BadCommandException { + if (commandMap.size() != ARGUMENT_PAYLOAD_SIZE) { + throw new BadCommandException(INVALID_ARGUMENT_MSG); + } else if (!commandMap.containsKey(COMMAND_KEYWORD)) { + throw new BadCommandException(INVALID_COMMAND_MSG); + } + } + + /** + * Entry point to this command.
    + * Check the validity of commands and add into favorite list.
    + */ + @Override + public void execute() { + try { + validateCommand(getArguments()); + } catch (BadCommandException badCommandException) { + LOGGER.log(Level.INFO, INVALID_COMMAND_MSG); + UI.printErrorFor(badCommandException, INVALID_COMMAND_NOTES); + return; + } + try { + removeFavQuestion(getArguments().get(COMMAND_KEYWORD)); + } catch (TokenizerException tokenizerException) { + LOGGER.log(Level.WARNING, TOKENIZER_ERROR); + UI.printErrorFor(tokenizerException, TOKENIZER_ERROR); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + UI.printErrorFor(storageException, STORAGE_ERROR); + } catch (NumberFormatException numberFormatException) { + LOGGER.log(Level.INFO, WRONG_INDEX_MSG); + BadCommandException exception = new BadCommandException(WRONG_INDEX_MSG); + UI.printErrorFor(exception, INVALID_COMMAND_NOTES); + } catch (ReflectionException reflectionException) { + UI.printErrorFor(reflectionException, INVALID_COMMAND_NOTES); + } catch (BadCommandException badCommandException) { + LOGGER.log(Level.INFO, INVALID_COMMAND_MSG); + UI.printErrorFor(badCommandException, INVALID_COMMAND_NOTES); + } + } + + /** + * Remove the user input index from favorite list and print the question to be removed.
    + *
    + * A valid index will only be removed(i.e. passed validateCommand()) if the favorite list in not empty. + * + * @param questionIndex User input of the index of question to be removed from favorite list. + * @throws TokenizerException If there is error in tokenization of index + * @throws StorageException If there is error in storing the data + * @throws NumberFormatException If invalid input is given, expected a valid integer + * @throws ReflectionException If fav list is empty + * @throws BadCommandException If an invalid command is given + */ + public void removeFavQuestion(String questionIndex) throws TokenizerException, StorageException, + NumberFormatException, ReflectionException, BadCommandException { + int questionIndexInt = Integer.parseInt(questionIndex); + if (this.favQuestionIndexes.size() == EMPTY_LIST) { + UI.printOutputMessage(EMPTY_FAV_LIST_MSG); + return; + } + if (questionIndexInt > this.favQuestionIndexes.size() || questionIndexInt < LOWER_BOUND) { + throw new ReflectionException(WRONG_INDEX_OUT_BOUND); + } + IndexMapper indexMapper = new IndexMapper(this.favQuestionIndexes); + HashMap indexQuestionMap = indexMapper.mapIndex(); + int indexToRemove = indexQuestionMap.get(questionIndexInt); + questionList.removeFavListIndex(indexToRemove); + } +} diff --git a/src/main/java/wellnus/reflection/feature/IndexMapper.java b/src/main/java/wellnus/reflection/feature/IndexMapper.java new file mode 100644 index 0000000000..50d87ea3be --- /dev/null +++ b/src/main/java/wellnus/reflection/feature/IndexMapper.java @@ -0,0 +1,31 @@ +package wellnus.reflection.feature; + +import java.util.HashMap; +import java.util.Set; + +/** + * Map display index(1...n) onto a set of integers. + */ +public class IndexMapper { + private static final int INDEX_ONE = 1; + private Set targetedSet; + public IndexMapper(Set targetedSet) { + this.targetedSet = targetedSet; + } + + /** + * The display index(integer) ranges from 1 to n.
    + * This function maps display index to each integer in the set. + * + * @return indexMap The hashmap with display index as key and real integer as value. + */ + public HashMap mapIndex() { + HashMap indexMap = new HashMap<>(); + int displayIndex = INDEX_ONE; + for (int index : this.targetedSet) { + indexMap.put(displayIndex, index); + displayIndex += INDEX_ONE; + } + return indexMap; + } +} diff --git a/src/main/java/wellnus/reflection/feature/QuestionList.java b/src/main/java/wellnus/reflection/feature/QuestionList.java new file mode 100644 index 0000000000..d74d980699 --- /dev/null +++ b/src/main/java/wellnus/reflection/feature/QuestionList.java @@ -0,0 +1,285 @@ +package wellnus.reflection.feature; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.common.WellNusLogger; +import wellnus.exception.StorageException; +import wellnus.exception.TokenizerException; +import wellnus.storage.ReflectionTokenizer; +import wellnus.storage.Storage; + +/** + * This class contains the list of questions available in reflect feature, + * and the list of indexes of favorite questions liked by the user.
    + *
    + * This class calls methods to load the list of indexes of favorite questions from data file, + * and save the updated data into data file.
    + *
    + * It also stores the indexes of the previous set of questions(i.e. set of 5 random indexes) + * which will then be used for other commands. + */ +public class QuestionList { + + // Questions are adopted from website: https://www.usa.edu/blog/self-discovery-questions/ + private static final String[] QUESTIONS = { + "What are three of my most cherished personal values?", + "What is my purpose in life?", + "What is my personality type?", + "Did I make time for myself this week?", + "Am I making time for my social life?", + "What scares me the most right now?", + "What is something I find inspiring?", + "What is something that brings me joy?", + "When is the last time I gave back to others?", + "What matters to me most right now?" + }; + private static final int TOTAL_NUM_QUESTIONS = 10; + private static final int RANDOM_NUMBER_UPPERBOUND = 10; + private static final int INDEX_ZERO = 0; + private static final int INDEX_ONE = 1; + private static final int INCREMENT_ONE = 1; + private static final String TOTAL_NUM_QUESTION_ASSERTIONS = "The total number of questions is 10."; + private static final String ADD_FAV_SUCCESS_ONE = "You have added question: "; + private static final String ADD_FAV_SUCCESS_TWO = " Into favorite list!!"; + private static final String REMOVE_FAV_SUCCESS_ONE = "You have removed question: "; + private static final String REMOVE_FAV_SUCCESS_TWO = " From favorite list!!"; + private static final String DUPLICATE_LIKE = " Is already in the favorite list!"; + private static final String STORAGE_ERROR = "Error storing data!"; + private static final String TOKENIZER_ERROR = "Previous reflect data will not be restored."; + private static final String DOT = "."; + private static final String EMPTY_STRING = ""; + private static final String FILE_NAME = "reflect"; + private static final String QUOTE = "\""; + private static final RandomNumberGenerator RANDOM_NUMBER_GENERATOR = + new RandomNumberGenerator(RANDOM_NUMBER_UPPERBOUND); + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectQuestionListLogger"); + private static final ReflectionTokenizer reflectionTokenizer = new ReflectionTokenizer(); + private static final ReflectUi UI = new ReflectUi(); + private static final boolean HAS_RANDOM_QUESTIONS = true; + private static final boolean NOT_HAS_RANDOM_QUESTIONS = false; + private static final boolean HAS_FAV_QUESTIONS = true; + private static final boolean NOT_HAS_FAV_QUESTIONS = false; + private ArrayList questions = new ArrayList<>(); + private Set randomQuestionIndexes; + private ArrayList> dataIndex; + private Storage storage; + + //@@author wenxin-c + /** + * Constructor to create a SelfReflection object and set up the questions available. + */ + public QuestionList() { + try { + storage = new Storage(); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + UI.printErrorFor(storageException, STORAGE_ERROR); + } + this.randomQuestionIndexes = new HashSet<>(); + this.dataIndex = new ArrayList<>(); + HashSet setLike = new HashSet<>(); + HashSet setPrev = new HashSet<>(); + this.dataIndex.add(setLike); + this.dataIndex.add(setPrev); + try { + this.loadQuestionData(); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + UI.printErrorFor(storageException, STORAGE_ERROR); + } catch (TokenizerException tokenizerException) { + overrideErrorReflectData(); + LOGGER.log(Level.WARNING, TOKENIZER_ERROR); + UI.printErrorFor(tokenizerException, TOKENIZER_ERROR); + } + setUpQuestions(); + assert questions.size() == TOTAL_NUM_QUESTIONS : TOTAL_NUM_QUESTION_ASSERTIONS; + } + + private void overrideErrorReflectData() { + ArrayList emptyTokenizedReflectIndexes = new ArrayList<>(); + try { + storage.saveData(emptyTokenizedReflectIndexes , Storage.FILE_REFLECT); + } catch (StorageException storageException) { + LOGGER.log(Level.WARNING, STORAGE_ERROR); + } + } + + /** + * Load the pool of introspective questions available for users. + */ + public void setUpQuestions() { + for (String question : QUESTIONS) { + addReflectQuestion(question); + } + } + + /** + * Create a ReflectionQuestion object for each question and add to questions list. + * + * @param question String of question description. + */ + public void addReflectQuestion(String question) { + ReflectionQuestion newQuestion = new ReflectionQuestion(question); + questions.add(newQuestion); + } + + public void setDataIndex(ArrayList> dataIndex) { + this.dataIndex = dataIndex; + } + + public ArrayList> getDataIndex() { + return dataIndex; + } + + /** + * Tokenize the indexes of liked questions and store them in a data file. + * + * @throws TokenizerException If there is error during tokenization + * @throws StorageException If data cannot be stored properly + */ + public void storeQuestionData() throws StorageException { + ArrayList tokenizedQuestionList = reflectionTokenizer.tokenize(this.dataIndex); + storage.saveData(tokenizedQuestionList, FILE_NAME); + } + + /** + * Load a string of integers from data file and detokenize it into the set of indexes of favorite questions. + * + * @throws StorageException If there is error during tokenization + * @throws TokenizerException If there is error during detokenization + */ + public void loadQuestionData() throws StorageException, TokenizerException { + ArrayList loadedQuestionList = storage.loadData(FILE_NAME); + ArrayList> detokenizedQuestionList = reflectionTokenizer.detokenize(loadedQuestionList); + this.setDataIndex(detokenizedQuestionList); + this.randomQuestionIndexes = this.dataIndex.get(INDEX_ONE); + } + + /** + * Generate a set of 5 distinct random numbers from 0-9 which will then be used as indexes to + * select 5 random questions. + */ + public void setRandomQuestionIndexes() throws StorageException { + this.randomQuestionIndexes = RANDOM_NUMBER_GENERATOR.generateRandomNumbers(); + ArrayList> updatedQuestionData = new ArrayList<>(); + Set favIndexList = this.dataIndex.get(INDEX_ZERO); + updatedQuestionData.add(favIndexList); + updatedQuestionData.add(this.randomQuestionIndexes); + this.setDataIndex(updatedQuestionData); + this.storeQuestionData(); + } + + public void setRandomQuestionIndexes(HashSet randomQuestionIndexes) { + this.randomQuestionIndexes = randomQuestionIndexes; + } + + public void clearRandomQuestionIndexes() { + this.randomQuestionIndexes.clear(); + } + + public Set getRandomQuestionIndexes() { + return this.randomQuestionIndexes; + } + + public ArrayList getAllQuestions() { + return questions; + } + + /** + * Add the index of a liked question into fav list.
    + *
    + * A valid index will only be added(i.e. passed validateCommand()) + * if the question is not yet in the favorite list.
    + * Indexes of all favorite questions will be stored in data file every time a question is liked. + * + * @param indexToAdd The index of the question liked by user + * @throws StorageException If data fails to be stored properly. + */ + public void addFavListIndex(int indexToAdd) throws StorageException { + if (this.dataIndex.get(INDEX_ZERO).contains(indexToAdd)) { + UI.printOutputMessage(QUOTE + questions.get(indexToAdd).toString() + QUOTE + DUPLICATE_LIKE); + return; + } + this.dataIndex.get(INDEX_ZERO).add(indexToAdd); + this.storeQuestionData(); + UI.printOutputMessage(ADD_FAV_SUCCESS_ONE + QUOTE + this.questions.get(indexToAdd).toString() + QUOTE + + ADD_FAV_SUCCESS_TWO); + } + + /** + * Remove the index of a liked question from the fav list.
    + *
    + * Indexes of all favorite questions will be stored in data file every time a question is removed. + * + * @param indexToRemove The index of question to be removed from fav list. + * @throws StorageException If data fails to be stored properly. + */ + public void removeFavListIndex(int indexToRemove) throws StorageException { + this.dataIndex.get(INDEX_ZERO).remove(indexToRemove); + this.storeQuestionData(); + UI.printOutputMessage(REMOVE_FAV_SUCCESS_ONE + QUOTE + this.questions.get(indexToRemove).toString() + QUOTE + + REMOVE_FAV_SUCCESS_TWO); + } + + /** + * Check whether a set of random question has been generated by checking the size of the set of question indexes. + * + * @return True for non-empty set and false for empty set + */ + public boolean hasRandomQuestionIndexes() { + if (this.randomQuestionIndexes.isEmpty()) { + return NOT_HAS_RANDOM_QUESTIONS; + } else { + return HAS_RANDOM_QUESTIONS; + } + } + + /** + * Check whether there is a set of favorite questions by checking the size of the set of favorite question indexes. + * + * @return True for non-empty set and false for empty set + */ + public boolean hasFavQuestions() { + if (this.dataIndex.get(INDEX_ZERO).isEmpty()) { + return NOT_HAS_FAV_QUESTIONS; + } else { + return HAS_FAV_QUESTIONS; + } + } + + /** + * Get a string of all favorite questions based on the favorite question indexes. + * + * @return String of favorite questions + */ + public String getFavQuestions() throws IndexOutOfBoundsException { + String questionString = EMPTY_STRING; + int displayIndex = INDEX_ONE; + for (int questionIndex : this.dataIndex.get(INDEX_ZERO)) { + questionString += (displayIndex + DOT + this.questions.get(questionIndex).toString() + + System.lineSeparator()); + displayIndex += INCREMENT_ONE; + } + return questionString; + } + + /** + * Get the previously generated set of questions. * + * @return String of previously generated questions */ + public String getPrevSetQuestions() throws IndexOutOfBoundsException { + String questionString = EMPTY_STRING; + int displayIndex = INDEX_ONE; + for (int questionIndex : this.dataIndex.get(INDEX_ONE)) { + questionString += (displayIndex + DOT + this.questions.get(questionIndex).toString() + + System.lineSeparator()); + displayIndex += INCREMENT_ONE; + } + return questionString; + } + //@@author +} + diff --git a/src/main/java/wellnus/reflection/feature/RandomNumberGenerator.java b/src/main/java/wellnus/reflection/feature/RandomNumberGenerator.java new file mode 100644 index 0000000000..8f84231c8a --- /dev/null +++ b/src/main/java/wellnus/reflection/feature/RandomNumberGenerator.java @@ -0,0 +1,46 @@ +package wellnus.reflection.feature; + +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; + +//@@author wenxin-c +/** + * Generate a set of 5 distinct random integers ranging from 0 to 9
    + *
    + * This set of random numbers will be used as indexes to get a set of random questions. + */ +public class RandomNumberGenerator { + private static final String NUM_SELECTED_QUESTIONS_ASSERTION = "The number of selected questions should be 5."; + private static final int LOWER_BOUND = 0; + private static final int ONE_OFFSET = 1; + private static final int NUM_OF_RANDOM_NUMBERS = 5; + private int upperBound; + + /** + * Constructor with the upper limit of the random number as an argument. + * + * @param upperBound The max value of the random number is (upperBound - 1) + */ + public RandomNumberGenerator(int upperBound) { + this.upperBound = upperBound; + } + + /** + * Generate a set of 5 random numbers.
    + *
    + * Each number num: num >= 0 and num <= (maxSize - 1) + * + * @return Set of 5 random numbers + */ + public Set generateRandomNumbers() { + Set randomNumbers = new Random().ints(LOWER_BOUND, this.upperBound - ONE_OFFSET) + .distinct() + .limit(NUM_OF_RANDOM_NUMBERS) + .boxed() + .collect(Collectors.toSet()); + assert randomNumbers.size() == NUM_OF_RANDOM_NUMBERS : NUM_SELECTED_QUESTIONS_ASSERTION; + return randomNumbers; + } +} + diff --git a/src/main/java/wellnus/reflection/feature/ReflectUi.java b/src/main/java/wellnus/reflection/feature/ReflectUi.java new file mode 100644 index 0000000000..97112b415b --- /dev/null +++ b/src/main/java/wellnus/reflection/feature/ReflectUi.java @@ -0,0 +1,28 @@ +package wellnus.reflection.feature; + +import wellnus.ui.TextUi; + +//@@author wenxin-c +/** + * This section is to be updated with main UI class + */ +public class ReflectUi extends TextUi { + private static final String SEPARATOR = "="; + + /** + * Call setSeparator() method inherited from TextUi superclass to re-define separator. + */ + public ReflectUi() { + setSeparator(SEPARATOR); + } + + private void printLogo(String logo) { + System.out.print(logo); + } + + protected void printLogoWithSeparator(String logo) { + printSeparator(); + printLogo(logo); + } +} + diff --git a/src/main/java/wellnus/reflection/feature/ReflectionManager.java b/src/main/java/wellnus/reflection/feature/ReflectionManager.java new file mode 100644 index 0000000000..6947696464 --- /dev/null +++ b/src/main/java/wellnus/reflection/feature/ReflectionManager.java @@ -0,0 +1,210 @@ +package wellnus.reflection.feature; + +import java.util.HashMap; +import java.util.NoSuchElementException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.common.WellNusLogger; +import wellnus.exception.BadCommandException; +import wellnus.manager.Manager; +import wellnus.reflection.command.FavoriteCommand; +import wellnus.reflection.command.GetCommand; +import wellnus.reflection.command.HelpCommand; +import wellnus.reflection.command.HomeCommand; +import wellnus.reflection.command.LikeCommand; +import wellnus.reflection.command.PrevCommand; +import wellnus.reflection.command.UnlikeCommand; + +/** + * The manager for self reflection section.
    + * This class oversees the command execution for self reflection section. + */ +public class ReflectionManager extends Manager { + public static final String FEATURE_HELP_DESCRIPTION = "reflect - Self Reflection - Take some time to pause " + + "and reflect with our specially curated list of questions and reflection management tools."; + public static final String FEATURE_NAME = "reflect"; + private static final Logger LOGGER = WellNusLogger.getLogger("ReflectionManagerLogger"); + private static final String GET_COMMAND = "get"; + private static final String HOME_COMMAND = "home"; + private static final String HELP_COMMAND = "help"; + private static final String LIKE_COMMAND = "like"; + private static final String UNLIKE_COMMAND = "unlike"; + private static final String FAV_COMMAND = "fav"; + private static final String PREV_COMMAND = "prev"; + private static final String NO_ELEMENT_MESSAGE = "There is no new line of input, please key in inputs!"; + private static final String INVALID_COMMAND_MESSAGE = "Invalid command issued!"; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String INVALID_COMMAND_NOTES = + "Supported commands in Self Reflection: " + LINE_SEPARATOR + + "get command " + GetCommand.COMMAND_USAGE + LINE_SEPARATOR + + "like command " + LikeCommand.COMMAND_USAGE + LINE_SEPARATOR + + "unlike command " + UnlikeCommand.COMMAND_USAGE + LINE_SEPARATOR + + "fav command " + FavoriteCommand.COMMAND_USAGE + LINE_SEPARATOR + + "prev command " + PrevCommand.COMMAND_USAGE + LINE_SEPARATOR + + "help command " + HelpCommand.COMMAND_USAGE + LINE_SEPARATOR + + "home command " + HomeCommand.COMMAND_USAGE; + private static final String COMMAND_TYPE_ASSERTION = "Command type should have length greater than 0"; + private static final String ARGUMENT_PAYLOAD_ASSERTION = "Argument-payload pairs cannot be empty"; + private static final String LOGO = + " ##### ###### \n" + + " # # ###### # ###### # # ###### ###### # ###### #### ##### \n" + + " # # # # # # # # # # # # # \n" + + " ##### ##### # ##### ###### ##### ##### # ##### # # \n" + + " # # # # # # # # # # # # \n" + + " # # # # # # # # # # # # # # \n" + + " ##### ###### ###### # # # ###### # ###### ###### #### # \n"; + private static final String GREETING_MESSAGE = "Welcome to WellNUS++ Self Reflection section :D" + + System.lineSeparator() + "Feel very occupied and cannot find time to self reflect?" + + System.lineSeparator() + "No worries, this section will give you the opportunity to reflect " + + "and improve on yourself!!"; + private static final int EMPTY_COMMAND = 0; + private static final boolean IS_EXIT_INITIAL = false; + private static final ReflectUi UI = new ReflectUi(); + private static boolean isExit; + private String commandType; + private HashMap argumentPayload; + private QuestionList questionList = new QuestionList(); + + /** + * Constructor to set initial isExit status to false and load the reflection questions. + */ + public ReflectionManager() { + setIsExit(IS_EXIT_INITIAL); + this.UI.setCursorName(FEATURE_NAME); + } + + public static void setIsExit(boolean status) { + isExit = status; + } + + public static boolean getIsExit() { + return isExit; + } + + public HashMap getArgumentPayload() { + return argumentPayload; + } + + public String getCommandType() { + return commandType; + } + + /** + * Get Self Reflection feature name. + * + * @return Feature name: reflect + */ + @Override + public String getFeatureName() { + return FEATURE_NAME; + } + + /** + * Set command argument and payload pairs from user inputs.
    + * This is to be used to generate command. + * + * @param inputCommand Read from user input + * @throws BadCommandException If an invalid command was given + */ + public void setArgumentPayload(String inputCommand) throws BadCommandException { + argumentPayload = commandParser.parseUserInput(inputCommand); + assert argumentPayload.size() > EMPTY_COMMAND : ARGUMENT_PAYLOAD_ASSERTION; + } + + /** + * Set the main command type to determine which command to create. + * + * @param inputCommand Read from user input + * @throws BadCommandException If an invalid command was given + */ + public void setCommandType(String inputCommand) throws BadCommandException { + commandType = commandParser.getMainArgument(inputCommand); + assert commandType.length() > EMPTY_COMMAND : COMMAND_TYPE_ASSERTION; + } + + //@@author wenxin-c + + /** + * Print greeting logo and message. + */ + public void greet() { + UI.printLogoWithSeparator(LOGO); + UI.printOutputMessage(GREETING_MESSAGE); + } + //@@author + + //@@author wenxin-c + + /** + * Main entry point of self reflection section.
    + *
    + * It prints out greeting messages, listen to and execute user commands. + */ + @Override + public void runEventDriver() { + setIsExit(false); + this.greet(); + while (!isExit) { + try { + String inputCommand = UI.getCommand(); + setCommandType(inputCommand); + setArgumentPayload(inputCommand); + executeCommands(); + } catch (NoSuchElementException noSuchElement) { + LOGGER.log(Level.INFO, NO_ELEMENT_MESSAGE); + UI.printErrorFor(noSuchElement, NO_ELEMENT_MESSAGE); + } catch (BadCommandException badCommand) { + LOGGER.log(Level.INFO, badCommand.getMessage()); + UI.printErrorFor(badCommand, INVALID_COMMAND_NOTES); + } + } + } + //@@author + + /** + * Decide which command to create based on command type.
    + *
    + * Commands available at this moment are: + *
  • Get a random set of reflection questions
    + *
  • Return back main interface
    + * + * @throws BadCommandException If an invalid command was given + */ + public void executeCommands() throws BadCommandException { + assert commandType.length() > EMPTY_COMMAND : COMMAND_TYPE_ASSERTION; + switch (commandType) { + case GET_COMMAND: + GetCommand getQuestionsCmd = new GetCommand(argumentPayload, questionList); + getQuestionsCmd.execute(); + break; + case HELP_COMMAND: + HelpCommand helpCmd = new HelpCommand(argumentPayload); + helpCmd.execute(); + break; + case HOME_COMMAND: + HomeCommand returnCmd = new HomeCommand(argumentPayload, questionList); + returnCmd.execute(); + break; + case LIKE_COMMAND: + LikeCommand likeCmd = new LikeCommand(argumentPayload, questionList); + likeCmd.execute(); + break; + case UNLIKE_COMMAND: + UnlikeCommand unlikeCmd = new UnlikeCommand(argumentPayload, questionList); + unlikeCmd.execute(); + break; + case FAV_COMMAND: + FavoriteCommand favCmd = new FavoriteCommand(argumentPayload, questionList); + favCmd.execute(); + break; + case PREV_COMMAND: + PrevCommand prevCmd = new PrevCommand(argumentPayload, questionList); + prevCmd.execute(); + break; + default: + throw new BadCommandException(INVALID_COMMAND_MESSAGE); + } + } +} + diff --git a/src/main/java/wellnus/reflection/feature/ReflectionQuestion.java b/src/main/java/wellnus/reflection/feature/ReflectionQuestion.java new file mode 100644 index 0000000000..fb7d1d4ea3 --- /dev/null +++ b/src/main/java/wellnus/reflection/feature/ReflectionQuestion.java @@ -0,0 +1,32 @@ +package wellnus.reflection.feature; + +//@@author wenxin-c +/** + * ReflectQuestion class is used to create reflect question objects. + */ +public class ReflectionQuestion { + private static final int EMPTY_QUESTION = 0; + private static final String EMPTY_QUESTION_MSG = "Question description cannot be empty."; + private String questionDescription; + + /** + * Constructor to create a ReflectionQuestion object and initialise question description. + * + * @param questionDescription The reflection question description + */ + public ReflectionQuestion(String questionDescription) { + assert questionDescription.length() > EMPTY_QUESTION : EMPTY_QUESTION_MSG; + this.questionDescription = questionDescription; + } + + /** + * Convert each reflect question to a string to be printed. + * + * @return Question description with its status + */ + @Override + public String toString() { + return questionDescription; + } +} + diff --git a/src/main/java/wellnus/storage/AtomicHabitTokenizer.java b/src/main/java/wellnus/storage/AtomicHabitTokenizer.java new file mode 100644 index 0000000000..1b92dc1990 --- /dev/null +++ b/src/main/java/wellnus/storage/AtomicHabitTokenizer.java @@ -0,0 +1,126 @@ +package wellnus.storage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; + +import wellnus.atomichabit.feature.AtomicHabit; +import wellnus.exception.TokenizerException; + +/** + * Class to tokenize and detokenize the AtomicHabit list.
    + */ +public class AtomicHabitTokenizer implements Tokenizer { + private static final String DESCRIPTION_KEY = "description"; + private static final String COUNT_KEY = "count"; + private static final String PARAMETER_DELIMITER = "--"; + private static final String DETOKENIZE_ERROR_MESSAGE = "Invalid habit data '%s' found in storage!"; + private static final int INDEX_ZERO = 0; + private static final int INDEX_FIRST = 1; + private static final int NUM_ATOMIC_HABIT_PARAMETER = 2; + private static final String REGEX_NUMBER_AND_SYMBOL_ONLY_PATTERN = "^[\\d\\p{Punct}\\p{S}]*$"; + + private String[] splitTokenizedHabitIntoParameter(String tokenizedHabit) { + tokenizedHabit = tokenizedHabit.strip(); + int noLimit = -1; + String[] rawStrings = tokenizedHabit.split(PARAMETER_DELIMITER, noLimit); + rawStrings = Arrays.copyOfRange(rawStrings, INDEX_FIRST, rawStrings.length); + String[] cleanString = new String[rawStrings.length]; + for (int i = 0; i < rawStrings.length; ++i) { + String currentCommand = rawStrings[i]; + currentCommand = currentCommand.strip(); + cleanString[i] = currentCommand; + } + return cleanString; + } + + private String convertToBase(String habitName) { + return habitName.toLowerCase().replaceAll("\\s", ""); + } + + private ArrayList removeDuplicatedHabits(ArrayList uncheckedAtomicHabits) { + HashMap uniqueHabits = new LinkedHashMap<>(); + for (AtomicHabit habit : uncheckedAtomicHabits) { + String description = convertToBase(habit.getDescription()); + if (!uniqueHabits.containsKey(description)) { + uniqueHabits.put(description, habit); + } + } + return new ArrayList<>(uniqueHabits.values()); + } + + private AtomicHabit parseTokenizedHabit(String tokenizedHabit) throws TokenizerException { + HashMap parameterHashMap = new HashMap<>(); + String[] parameterStrings = splitTokenizedHabitIntoParameter(tokenizedHabit); + try { + for (String parameterString : parameterStrings) { + int i = parameterString.indexOf(' '); + String parameterKey = parameterString.substring(INDEX_ZERO, i); + String parameterValue = parameterString.substring(i).trim(); + parameterHashMap.put(parameterKey, parameterValue); + } + } catch (StringIndexOutOfBoundsException stringIndexOutOfBoundsException) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, tokenizedHabit)); + } + if ((!parameterHashMap.containsKey(DESCRIPTION_KEY) || !parameterHashMap.containsKey(COUNT_KEY)) + && !parameterHashMap.isEmpty()) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, tokenizedHabit)); + } + if (parameterHashMap.size() != NUM_ATOMIC_HABIT_PARAMETER) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, tokenizedHabit)); + } + String description = parameterHashMap.get(DESCRIPTION_KEY); + String countString = parameterHashMap.get(COUNT_KEY); + if (description.matches(REGEX_NUMBER_AND_SYMBOL_ONLY_PATTERN)) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, tokenizedHabit)); + } + try { + int count = Integer.parseInt(countString); + return new AtomicHabit(description, count); + } catch (NumberFormatException numberFormatException) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, tokenizedHabit)); + } + } + + /** + * Tokenize List of Atomic Habits to be saved as ArrayList of Strings.
    + * Each habit will be tokenized with the following format: + * --description [description of habit] --count [count of habit].
    + * + * @param habitsToTokenize List of atomic habits to be tokenized as ArrayList of strings. + * @return ArrayList of Strings representing the tokenized habits that we can write to storage. + */ + public ArrayList tokenize(ArrayList habitsToTokenize) { + ArrayList tokenizedHabits = new ArrayList<>(); + for (AtomicHabit habit : habitsToTokenize) { + String tokenizedHabit = PARAMETER_DELIMITER + DESCRIPTION_KEY + + " " + habit.getDescription() + + " " + PARAMETER_DELIMITER + COUNT_KEY + + " " + habit.getCount(); + tokenizedHabits.add(tokenizedHabit); + } + return tokenizedHabits; + } + + /** + * Convert strings of tokenized AtomicHabit into ArrayList of AtomicHabit.
    + * This method can be called in the constructor of AtomicHabitManager to detokenize. + * ArrayList of atomic habits from storage.
    + * + * @param tokenizedAtomicHabits List of tokenized atomic habit strings from the storage. + * @return ArrayList containing all the atomic habit saved in the storage. + * @throws TokenizerException When the data can't be detokenized. + */ + public ArrayList detokenize(ArrayList tokenizedAtomicHabits) throws TokenizerException { + ArrayList detokenizedAtomicHabits = new ArrayList<>(); + for (String tokenizedString : tokenizedAtomicHabits) { + if (!tokenizedString.isBlank()) { + AtomicHabit parsedHabit = parseTokenizedHabit(tokenizedString); + detokenizedAtomicHabits.add(parsedHabit); + } + } + detokenizedAtomicHabits = removeDuplicatedHabits(detokenizedAtomicHabits); + return detokenizedAtomicHabits; + } +} diff --git a/src/main/java/wellnus/storage/GamificationTokenizer.java b/src/main/java/wellnus/storage/GamificationTokenizer.java new file mode 100644 index 0000000000..0a905d930d --- /dev/null +++ b/src/main/java/wellnus/storage/GamificationTokenizer.java @@ -0,0 +1,67 @@ +package wellnus.storage; + +import java.util.ArrayList; + +import wellnus.exception.TokenizerException; +import wellnus.gamification.util.GamificationData; + +/** + * Handles the conversion of GamificationData objects -> String and vice versa to allow + * storage and retrieval of gamification statistics. + */ +public class GamificationTokenizer implements Tokenizer { + private static final String INVALID_STORED_DATA_MESSAGE = "Invalid gamification data '%s' found in storage!"; + private static final int MIN_XP = 0; + + /** + * Converts the attributes of the GamificationManager into a String representation to be + * saved to storage. + * + * @param dataObjects List of GamificationData Objects we want to convert into a String representation + * @return ArrayList of Strings representing the GamificationData objects that we can write to storage + */ + @Override + public ArrayList tokenize(ArrayList dataObjects) { + ArrayList tokenizedObjects = new ArrayList<>(); + for (GamificationData data : dataObjects) { + int xp = data.getTotalXp(); + String tokenizedObject = "" + xp; + tokenizedObjects.add(tokenizedObject); + } + return tokenizedObjects; + } + + /** + * Converts the String representation of the GamificationManager's state back into an + * ArrayList of GamificationData that can be used to restore the gamification feature's + * previous state. + * + * @param tokenizedDataObjects String representation of the GamificationData Objects whose state we want to restore + * @return ArrayList containing all the gamification data from the gamification feature's previously saved state + * @throws TokenizerException If detokenizing fails and stored gamification statistics cannot be restored + */ + @Override + public ArrayList detokenize(ArrayList tokenizedDataObjects) + throws TokenizerException { + ArrayList dataObjects = new ArrayList<>(); + for (String tokenizedDataObject : tokenizedDataObjects) { + // Data file contains blank lines + if (tokenizedDataObject.isBlank()) { + // Ignore the blank line and check other lines in the data file + continue; + } + int totalXp; + try { + totalXp = Integer.parseInt(tokenizedDataObject.trim()); + } catch (NumberFormatException numberFormatException) { + throw new TokenizerException(String.format(INVALID_STORED_DATA_MESSAGE, tokenizedDataObject)); + } + if (totalXp < MIN_XP) { + throw new TokenizerException(String.format(INVALID_STORED_DATA_MESSAGE, totalXp + "")); + } + GamificationData data = new GamificationData(totalXp); + dataObjects.add(data); + } + return dataObjects; + } +} diff --git a/src/main/java/wellnus/storage/ReflectionTokenizer.java b/src/main/java/wellnus/storage/ReflectionTokenizer.java new file mode 100644 index 0000000000..5d3bcd0703 --- /dev/null +++ b/src/main/java/wellnus/storage/ReflectionTokenizer.java @@ -0,0 +1,150 @@ +package wellnus.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import wellnus.exception.TokenizerException; + +/** + * Class to tokenize and detokenize the Index for 'like' and 'prev' command in Reflection Feature.
    + */ +public class ReflectionTokenizer implements Tokenizer> { + private static final String INDEX_DELIMITER = ","; + private static final int INDEX_ZERO = 0; + private static final int INDEX_ONE = 1; + private static final int LIKE_INDEX = 0; + private static final int PREV_INDEX = 1; + private static final int INDEX_NINE = 9; + private static final int NUM_PREV_INDEX = 5; + private static final int TOKENIZER_INDEX_ARRAYLIST_SIZE = 2; + private static final String LIKE_KEY = "like"; + private static final String PREV_KEY = "prev"; + private static final String COLON_CHARACTER = ":"; + private static final int NO_LIMIT = -1; + private static final String DETOKENIZE_ERROR_MESSAGE = "Invalid reflect %s data '%s' found in storage!"; + private static final String INVALID_NUM_OF_LINES_ERRROR = "Invalid reflect data formatting found in storage!"; + private String getTokenizedIndexes(String key, Set indexesToTokenize) { + String tokenizedIndexes = key + COLON_CHARACTER; + for (int index : indexesToTokenize) { + tokenizedIndexes = tokenizedIndexes + index + INDEX_DELIMITER; + } + if (indexesToTokenize.size() != INDEX_ZERO) { + tokenizedIndexes = tokenizedIndexes.substring(INDEX_ZERO, tokenizedIndexes.length() - INDEX_ONE); + } + return tokenizedIndexes; + } + + private String splitParameter(String tokenizedRawString, String categoryKey) throws TokenizerException { + int indexSplit = tokenizedRawString.indexOf(COLON_CHARACTER); + String parameter; + String tokenizedIndexes; + try { + parameter = tokenizedRawString.substring(INDEX_ZERO, indexSplit); + if (!parameter.equals(categoryKey)) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, categoryKey, tokenizedRawString)); + } + tokenizedIndexes = tokenizedRawString.substring(indexSplit + INDEX_ONE).trim(); + } catch (StringIndexOutOfBoundsException stringIndexOutOfBoundsException) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, categoryKey, tokenizedRawString)); + } + return tokenizedIndexes; + } + + private String[] splitTokenizedIndex(String tokenizedIndexes) { + tokenizedIndexes = tokenizedIndexes.strip(); + String[] inputStrings = tokenizedIndexes.split(INDEX_DELIMITER, NO_LIMIT); + String[] outputStrings = new String[inputStrings.length]; + for (int i = 0; i < inputStrings.length; ++i) { + String currentCommand = inputStrings[i]; + currentCommand = currentCommand.strip(); + outputStrings[i] = currentCommand; + } + return outputStrings; + } + + private Set validateTokenizedIndexFormat(ArrayList tokenizedIndex, + int categoryIndex, String categoryKey) throws TokenizerException { + Set validatedSet = new HashSet<>(); + String tokenizedIndexesByCategory = tokenizedIndex.get(categoryIndex); + if (!tokenizedIndexesByCategory.isBlank()) { + String rawIndex = splitParameter(tokenizedIndexesByCategory, categoryKey); + validatedSet = getSet(rawIndex, categoryKey); + } + return validatedSet; + } + + private Set getSet(String indexToSplit, String categoryKey) throws TokenizerException { + Set outputIndexes = new HashSet<>(); + if (indexToSplit.isBlank()) { + return outputIndexes; + } + String[] splittedString = splitTokenizedIndex(indexToSplit); + try { + for (String indexString : splittedString) { + int index = Integer.parseInt(indexString); + if (index < INDEX_ZERO || index > INDEX_NINE) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, categoryKey, indexToSplit)); + } + outputIndexes.add(index); + } + } catch (NumberFormatException numberFormatException) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, categoryKey, indexToSplit)); + } + if (categoryKey.equals(PREV_KEY) && outputIndexes.size() != NUM_PREV_INDEX) { + throw new TokenizerException(String.format(DETOKENIZE_ERROR_MESSAGE, categoryKey, indexToSplit)); + } + return outputIndexes; + } + + /** + * Tokenize ArrayList of Set of Integers into strings that can be stored.
    + * ArrayList contains 2 Set of Integers, which corresponds for set of like indexes for the first entry + * and set of prev indexes for second entry.
    + * Each index will be tokenized with the following format: + * like:[list of comma separated index]
    + * prev:[list of comma separated index]
    + * + * @param arrayIndexToTokenize ArrayList that contains set of like indexes for the first entry + * and set of prev indexes for the second entry.
    + * @return ArrayList of Strings representing the tokenized like indexes and prev indexes that we can + * write to storage. + */ + public ArrayList tokenize(ArrayList> arrayIndexToTokenize) { + ArrayList tokenizedIndexes = new ArrayList<>(); + Set likeIndexToTokenize = arrayIndexToTokenize.get(INDEX_ZERO); + Set prevIndexToTokenize = arrayIndexToTokenize.get(INDEX_ONE); + String tokenizedLike = getTokenizedIndexes(LIKE_KEY, likeIndexToTokenize); + String tokenizedPrev = getTokenizedIndexes(PREV_KEY, prevIndexToTokenize); + tokenizedIndexes.add(tokenizedLike); + tokenizedIndexes.add(tokenizedPrev); + return tokenizedIndexes; + } + + /** + * Convert strings of tokenized Indexes into ArrayList that contains set of like indexes for the first entry + * and set of prev indexes for the second entry.
    + * This method can be called in the constructor of ReflectionManager to detokenize. + * ArrayList of indexes from storage.
    + * + * @param tokenizedIndex List of tokenized like and prev indexes from the storage. + * @return ArrayList that contains set of like indexes for the first entry + * and set of prev indexes for the second entry
    + * @throws TokenizerException when the data can't be detokenized. + */ + public ArrayList> detokenize(ArrayList tokenizedIndex) throws TokenizerException { + ArrayList> detokenizedIndexes = new ArrayList<>(); + Set detokenizedLike = new HashSet<>(); + Set detokenizedPrev = new HashSet<>(); + if (tokenizedIndex.size() == TOKENIZER_INDEX_ARRAYLIST_SIZE) { + detokenizedLike = validateTokenizedIndexFormat(tokenizedIndex, LIKE_INDEX, LIKE_KEY); + detokenizedPrev = validateTokenizedIndexFormat(tokenizedIndex, PREV_INDEX, PREV_KEY); + } else if (tokenizedIndex.size() > INDEX_ZERO && !tokenizedIndex.get(INDEX_ZERO).isBlank()) { + throw new TokenizerException(INVALID_NUM_OF_LINES_ERRROR); + } + detokenizedIndexes.add(detokenizedLike); + detokenizedIndexes.add(detokenizedPrev); + return detokenizedIndexes; + } +} + diff --git a/src/main/java/wellnus/storage/Storage.java b/src/main/java/wellnus/storage/Storage.java new file mode 100644 index 0000000000..2314763ccf --- /dev/null +++ b/src/main/java/wellnus/storage/Storage.java @@ -0,0 +1,375 @@ +package wellnus.storage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.common.WellNusLogger; +import wellnus.exception.StorageException; + +//@@author nichyjt + +/** + * Storage is the common interface for all Features to save and load data from.
    + *

    + * To save data, the manager should call saveData() and input a list of Strings. + * Each string represents one instance of the tokenized form of the data structure the manager is handling, + * such as ReflectionQuestion or AtomicHabit.
    + *

    + * To load data, the manager should call loadData() and input the correct filename of + * the data to be loaded. The filename should be obtained from the public constant Storage.FILE_[name]. + *

    + */ +public class Storage { + // These constant strings are intentionally made public + // to allow any FeatureManager to call the (de)tokenize functions with the correct filename + public static final String FILE_HABIT = "habit"; + public static final String FILE_REFLECT = "reflect"; + public static final String FILE_GAMIFICATION = "gamif"; + protected static final String FILE_DEBUG = "debug"; + + // Delimiter constants + protected static final String DELIMITER = " --" + System.lineSeparator(); + protected static final String NEWLINE = System.lineSeparator(); + private static final String FILE_EXTENTION = ".txt"; + private static final String WORKING_DIRECTORY = "."; + private static final String DATA_DIRECTORY_NAME = "data"; + + // Message constants + private static final String ERROR_GENERAL = "WellNUS++ faced an internal error in storage!"; + private static final String ERROR_CANNOT_MAKE_FILE = "WellNUS++ couldn't make the data file!"; + private static final String ERROR_CANNOT_MAKE_DIR = "WellNUS++ couldn't make the data directory!"; + private static final String ERROR_CANNOT_DELETE_FILE = "WellNUS++ couldn't delete the data file!"; + private static final String ERROR_CANNOT_RESOLVE_PATH = "WellNUS++ couldn't resolve a path internally!"; + private static final String ERROR_CANNOT_WRITE_FILE = "WellNUS++ couldn't write to a file!"; + private static final String ERROR_CANNOT_LOAD_FILE = "WellNUS++ couldn't load a file!"; + private static final String ERROR_INVALID_FILENAME = "WellNUS++ cannot create a file that is not registered!"; + private static final String ASSERT_FILENAME_NOT_NULL = "fileName should not be null!"; + private static final String ASSERT_FILENAME_NOT_EMPTY = "fileName should have a length > 0!"; + private static final String ASSERT_PATH_NOT_NULL = "path should not be null!"; + private static final String ASSERT_LIST_NOT_NULL = "list input should not be null!"; + private static final String ASSERT_STRING_NOT_NULL = "string input should not be null!"; + private static final String ASSERT_FILE_NOT_NULL = "file input should not be null!"; + private static final Logger LOGGER = WellNusLogger.getLogger("StorageLogger"); + private static final String LOG_ACCESS_ERROR = "WellNUS++ has encountered a severe input/output error! \n" + + "Check if file permissions and data directory are properly instantiated?"; + private static final String LOG_MISSING_FILE = "WellNUS++ could not find a file.\n" + + "Check if this method was called before any data file instantiation?"; + private static final String LOG_INVALID_FILENAME = "WellNUS++ cannot create the file as its name is invalid.\n" + + "Check if its filename is registered in the Storage class."; + private static final int FILENAME_EMPTY = 0; + private Path wellNusDataDirectory; + + /** + * Construct an instance of Storage to call saveData and loadData from. + * + * @throws StorageException when creating the data directory fails + */ + //@@author nichyjt + public Storage() throws StorageException { + wellNusDataDirectory = Paths.get(WORKING_DIRECTORY, DATA_DIRECTORY_NAME); + // For safety, check that the data folder actually exists + // If it doesn't, create it. + verifyDataDirectory(); + } + + /** + * Method to check if the specific file exists. + * Used to check if the file exists before attempting to load it for atomic habit manager. + * + * @param fileName name of the file storing the feature data + * @return boolean representing if the file exists + * @throws StorageException When querying the fileName fails + */ + public boolean checkFileExists(String fileName) throws StorageException { + Path pathToFile; + File dataFile; + try { + pathToFile = wellNusDataDirectory.resolve(fileName + FILE_EXTENTION); + dataFile = pathToFile.toFile(); + } catch (InvalidPathException exception) { + String errorMessage = ERROR_CANNOT_RESOLVE_PATH; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } catch (UnsupportedOperationException exception) { + String errorMessage = ERROR_GENERAL; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } + return dataFile.exists(); + } + + /** + * Check if the supplied fileName is a valid WellNUS++ file. + * + * @param fileName name of file to be used in WellNUS++ + * @return boolean representing if the fileName exists + */ + //@@author nichyjt + private boolean isValidFileName(String fileName) { + assert fileName != null : ASSERT_STRING_NOT_NULL; + switch (fileName) { + case FILE_GAMIFICATION: + case FILE_HABIT: + case FILE_REFLECT: + case FILE_DEBUG: + // fallthrough + return true; + default: + return false; + } + } + + /** + * Check that the data folder exists. If it does not, try creating it. + * + * @throws StorageException if directory cannot be made + */ + //@@author nichyjt + private void verifyDataDirectory() throws StorageException { + assert wellNusDataDirectory != null : "wellNusDataDirectory path should be set up!"; + boolean directoryExists = Files.exists(wellNusDataDirectory); + if (directoryExists) { + return; + } + createDataFolder(wellNusDataDirectory); + } + + /** + * Creates a File relative to the data folder. + * + * @param fileName data file to retrieve + */ + protected File getFile(String fileName) throws StorageException { + assert fileName != null : ASSERT_FILENAME_NOT_NULL; + assert fileName.length() > FILENAME_EMPTY : ASSERT_FILENAME_NOT_EMPTY; + if (!isValidFileName(fileName)) { + LOGGER.log(Level.WARNING, LOG_INVALID_FILENAME); + throw new StorageException(ERROR_INVALID_FILENAME); + } + verifyDataDirectory(); + Path pathToFile; + File dataFile; + try { + pathToFile = wellNusDataDirectory.resolve(fileName + FILE_EXTENTION); + dataFile = pathToFile.toFile(); + } catch (InvalidPathException exception) { + String errorMessage = ERROR_CANNOT_RESOLVE_PATH; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } catch (UnsupportedOperationException exception) { + String errorMessage = ERROR_GENERAL; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } + boolean fileExists = dataFile.exists(); + if (!fileExists) { + createFile(dataFile); + } + return dataFile; + } + + /** + * Create the data folder for WellNUS++. + * + * @param directoryPath path of the directory + * @throws StorageException if directory cannot be made + */ + //@@author nichyjt + private void createDataFolder(Path directoryPath) throws StorageException { + assert directoryPath != null : ASSERT_PATH_NOT_NULL; + try { + Files.createDirectory(directoryPath); + } catch (IOException exception) { + LOGGER.log(Level.SEVERE, LOG_ACCESS_ERROR); + String errorMessage = ERROR_CANNOT_MAKE_DIR + System.lineSeparator(); + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } + } + + /** + * Create a file in the path specified by its URI. + * + * @param file to be created + * @throws StorageException if the file cannot be made + */ + //@@author nichyjt + private void createFile(File file) throws StorageException { + assert file != null : ASSERT_FILE_NOT_NULL; + try { + file.createNewFile(); + } catch (IOException exception) { + LOGGER.log(Level.SEVERE, LOG_ACCESS_ERROR); + String errorMessage = ERROR_CANNOT_MAKE_FILE; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } + } + + /** + * Tokenize every String entry with the delimiter suffix and append them together. + * + * @param tokenizedStrings strings to be tokenized + * @return String of all tokenized string entries + */ + //@@author nichyjt + protected String tokenizeStringList(ArrayList tokenizedStrings) { + assert tokenizedStrings != null : ASSERT_LIST_NOT_NULL; + StringBuilder stringBuilder = new StringBuilder(); + for (String entry : tokenizedStrings) { + String entryDelimited = entry + DELIMITER; + stringBuilder.append(entryDelimited); + } + return stringBuilder.toString(); + } + + /** + * Splits a dataString by the " --\n" delimiter. + * + * @param dataString string to be split + * @return String[] of words belonging to the dataString + */ + //@@author nichyjt + private String[] splitIntoEntries(String dataString) { + assert dataString != null : ASSERT_STRING_NOT_NULL; + return dataString.split(DELIMITER); + } + + /** + * Detokenizing raw dataString into ArrayList of strings, where each string + * is an entry in the associated Manager's data structure. + * + * @param dataString raw string loaded from the text file + * @return ArrayList of strings to be parsed by tokenizer + */ + //@@author nichyjt + protected ArrayList detokenizeDataString(String dataString) { + assert dataString != null : ASSERT_STRING_NOT_NULL; + String[] entries = splitIntoEntries(dataString); + return new ArrayList<>(Arrays.asList(entries)); + } + + private void writeDataToDisk(String data, File file) throws StorageException { + assert data != null : ASSERT_STRING_NOT_NULL; + assert file != null : ASSERT_FILE_NOT_NULL; + // assume file exists + try { + FileWriter writer = new FileWriter(file.getAbsolutePath()); + writer.write(data); + writer.close(); + } catch (IOException exception) { + LOGGER.log(Level.SEVERE, LOG_MISSING_FILE); + String errorMessage = ERROR_CANNOT_WRITE_FILE; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } + } + + private String loadDataFromDisk(File file) throws StorageException { + assert file != null : ASSERT_FILE_NOT_NULL; + // assume file exists + StringBuilder data = new StringBuilder(); + try { + Scanner reader = new Scanner(file); + while (reader.hasNextLine()) { + data.append(reader.nextLine()).append(NEWLINE); + } + reader.close(); + } catch (FileNotFoundException exception) { + LOGGER.log(Level.SEVERE, LOG_ACCESS_ERROR); + String errorMessage = ERROR_CANNOT_LOAD_FILE; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } catch (IllegalStateException exception) { + LOGGER.log(Level.SEVERE, LOG_ACCESS_ERROR); + String errorMessage = ERROR_GENERAL; + errorMessage = errorMessage.concat(exception.getMessage()); + throw new StorageException(errorMessage); + } + return data.toString(); + } + + /** + * Save the pre-tokenized data onto Disk.
    + *

    + * The data will be saved into the /data folder.
    + * Each entry in the ArrayList should be an instance of the underlying data structure being `Managed`, + * with each instance being tokenized into a String beforehand.
    + * The fileName should be accessed via the public constant Storage.FILE_[feature]. + * + * @param tokenizedManager ArrayList of tokenized Manager data string + * @param fileName name of the file to be saved + * @throws StorageException when there are unexpected IO errors + */ + //@@author nichyjt + public void saveData(ArrayList tokenizedManager, String fileName) throws StorageException { + assert fileName != null : ASSERT_FILENAME_NOT_NULL; + assert fileName.length() > FILENAME_EMPTY : ASSERT_FILENAME_NOT_EMPTY; + assert tokenizedManager != null : ASSERT_LIST_NOT_NULL; + if (!isValidFileName(fileName)) { + LOGGER.log(Level.WARNING, LOG_INVALID_FILENAME); + throw new StorageException(ERROR_INVALID_FILENAME); + } + File file = getFile(fileName); + String tokenizedString = tokenizeStringList(tokenizedManager); + writeDataToDisk(tokenizedString, file); + } + + /** + * Load a feature's data from the Disk.
    + *

    + * The data will be laoded from the /data folder.
    + * Each entry in the ArrayList will be an instance of the underlying data structure being `Managed`, + * with each instance being tokenized into a String beforehand
    + * The fileName should be accessed via the public constant Storage.FILE_[feature]. + * + * @param fileName name of the file to be loaded + * @return ArrayList of tokenized Manager data string + * @throws StorageException when there are unexpected IO errors + */ + //@@author nichyjt + public ArrayList loadData(String fileName) throws StorageException { + assert fileName != null : ASSERT_FILENAME_NOT_NULL; + assert fileName.length() > FILENAME_EMPTY : ASSERT_FILENAME_NOT_EMPTY; + if (!isValidFileName(fileName)) { + LOGGER.log(Level.WARNING, LOG_INVALID_FILENAME); + throw new StorageException(ERROR_INVALID_FILENAME); + } + File file = getFile(fileName); + String data = loadDataFromDisk(file); + return detokenizeDataString(data); + } + + /** + * Deletes the file from the /data directory. + * + * @param fileName name of the file to be deleted + * @throws StorageException when there are unexpected IO errors + */ + //@@author nichyjt + protected void deleteFile(String fileName) throws StorageException { + assert fileName != null : ASSERT_FILENAME_NOT_NULL; + assert fileName.length() > FILENAME_EMPTY : ASSERT_FILENAME_NOT_EMPTY; + if (!isValidFileName(fileName)) { + LOGGER.log(Level.WARNING, LOG_INVALID_FILENAME); + throw new StorageException(ERROR_INVALID_FILENAME); + } + File file = getFile(fileName); + boolean isDeleted = file.delete(); + if (!isDeleted) { + throw new StorageException(ERROR_CANNOT_DELETE_FILE); + } + } + +} diff --git a/src/main/java/wellnus/storage/Tokenizer.java b/src/main/java/wellnus/storage/Tokenizer.java new file mode 100644 index 0000000000..cb8a99a249 --- /dev/null +++ b/src/main/java/wellnus/storage/Tokenizer.java @@ -0,0 +1,37 @@ +package wellnus.storage; + +import java.util.ArrayList; + +import wellnus.exception.TokenizerException; + +/** + * Template for all Tokenizers in WellNUS++ that are responsible for converting + * Managers into Strings(for storage) and also Strings(from storage) back + * into Managers with the previously saved state.
    + * + * Example of how to implement this in a feature: public class AtomicHabitTokenizer + * implements Tokenizer<AtomicHabit>. + * @param Data type of the corresponding feature, e.g. AtomicHabit + * the atomic habit feature + */ +public interface Tokenizer { + /** + * Converts the attributes of the given Manager into a String representation to be + * saved to storage. + * @param dataObjects List of Objects which represent data we want to convert into a String representation + * @return ArrayList of Strings representing the data objects that we can write to storage + * @throws TokenizerException If tokenizing fails and state cannot be converted into a valid String + * representation + */ + ArrayList tokenize(ArrayList dataObjects); + + /** + * Converts the String representation of a Manager's state back into an + * ArrayList of the feature's data type class that can be used to restore that + * Manager's previous state. + * @param tokenizedDataObjects String representation of the Data Objects whose state we want to restore + * @return ArrayList containing all the data from the Manager's previously saved state + * @throws TokenizerException If detokenizing fails and valid state cannot be restored + */ + ArrayList detokenize(ArrayList tokenizedDataObjects) throws TokenizerException; +} diff --git a/src/main/java/wellnus/ui/TextUi.java b/src/main/java/wellnus/ui/TextUi.java new file mode 100644 index 0000000000..84f4bec3b9 --- /dev/null +++ b/src/main/java/wellnus/ui/TextUi.java @@ -0,0 +1,233 @@ +package wellnus.ui; + +import java.io.InputStream; +import java.nio.BufferOverflowException; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +import wellnus.common.WellNusLogger; + +//@@author wenxin-c + +/** + * TextUi class for reading user inputs and printing outputs.
    + *
    + * Subclasses of TextUI class can override separator, printErrorFor and printOutputMessage.
    + * This is to accommodate to the uniqueness of each feature. + */ +public class TextUi { + private static final Logger LOGGER = WellNusLogger.getLogger("TextUiLogger"); + private static final String ALERT_SEPARATOR = "!!!!!!-------!!!!!--------!!!!!!!------!!!!!" + + "---------!!!!!!!"; + private static final String INDENTATION_SPACES = " "; + private static final int DEFAULT_SEPARATOR_LENGTH = 60; + private static final int EMPTY_MESSAGE = 0; + private static final String ERROR_MESSAGE_LABEL = "Error Message:"; + private static final String ERROR_EMPTY_STRING = "The string argument should not be empty!"; + private static final String EXTRA_MESSAGE_LABEL = "Note:"; + private static final String NO_INPUT_ELEMENT_MSG = "There is no new line of element, " + + "please key in input!"; + private static final String BUFFER_OVERFLOW_MSG = "Input is too long, please shorten your input!"; + private static final String CURSOR_CARET = "(%s):~$ "; + private Scanner scanner; + private String separator = "-"; + private int separatorLength; + private String cursor = CURSOR_CARET; + + /** + * Returns a new instance of TextUi that reads user input from the default + * System.in InputStream. + */ + public TextUi() { + this(System.in); + } + + /** + * Returns a new instance of TextUi that reads user input from the given + * InputStream. + * + * @param inputStream InputStream that WellNUS++ will read user input(commands) from + */ + public TextUi(InputStream inputStream) { + this.scanner = new Scanner(inputStream); + this.separatorLength = DEFAULT_SEPARATOR_LENGTH; + } + + //@@author wenxin-c + + /** + * Print spaces before output message for better formatting. + */ + public void printIndentation() { + System.out.print(INDENTATION_SPACES); + } + + /** + * Read user's input command and return back the command string.
    + * + * @return User input command with leading/dangling whitespace being removed + */ + public String getCommand() { + printCursor(); + String userCommand = ""; + try { + String inputLine = scanner.nextLine(); + userCommand = inputLine.trim(); + } catch (BufferOverflowException bufferOverFlowException) { + LOGGER.log(Level.INFO, BUFFER_OVERFLOW_MSG); + printErrorFor(bufferOverFlowException, BUFFER_OVERFLOW_MSG); + } catch (NoSuchElementException noElementException) { + LOGGER.log(Level.INFO, NO_INPUT_ELEMENT_MSG); + printErrorFor(noElementException, NO_INPUT_ELEMENT_MSG); + } + return userCommand; + } + + /** + * Customise separators for each feature.
    + *
    + * At this moment we can only use length == 1 separator for consistency of length of line separator.
    + * This will be improved on in the future to allow for more patterns. + * + * @param separator Length == 1 string + */ + public void setSeparator(String separator) { + this.separator = separator; + } + + /** + * Customises the length of the separator as needed by a particular feature's + * unique style. + * + * @param separatorLength Number of characters to print in the feature's separator + */ + public void setSeparatorLength(int separatorLength) { + this.separatorLength = separatorLength; + } + + /** + * Print line separators for output lines.
    + *
    + * Each subclass inherited from this class can override this method to vary the interface. + */ + public void printSeparator() { + for (int i = 0; i < separatorLength; i += 1) { + System.out.print(separator); + } + System.out.print(System.lineSeparator()); + } + + /** + * Split a message string to a string array using System.lineSeparator().
    + *
    + * This is to help indent each new line during output.
    + * Each line of string will be trimmed to remove leading/dangling whitespace. + * + * @param message Message to be printed + * @return Messages that are split using lineSeparator + */ + private String[] splitOutputMessage(String message) { + String[] newLineMessages = message.trim().split(System.lineSeparator()); + for (int i = 0; i < newLineMessages.length; i += 1) { + newLineMessages[i] = newLineMessages[i].trim(); + } + return newLineMessages; + } + + /** + * Print each new line of message on a separate line with indentation being added.
    + *
    + * Output message with one line is also accepted. + * + * @param message Output message to be printed + */ + private void printMultiLineMessage(String message) { + String[] newLineMessages = splitOutputMessage(message); + for (String msg : newLineMessages) { + printIndentation(); + System.out.println(msg); + } + } + + /** + * Print exception message with length > 0 and additional notes.
    + *
    + * 0 or more lines of messages are accepted, but lineSeparator() must be added + * if you wish to have certain message to start on a new line.
    + * Error messages and additional notes will be printed on separate lines with labels.
    + * Can override to accommodate to other customised error messages.
    + * Can improve on what will be printed for empty message in the future. + * + * @param exception The exception being thrown in the program + * @param additionalMessage Suggestions or notes that help users figure out what causes error + */ + public void printErrorFor(Exception exception, String additionalMessage) { + System.out.println(ALERT_SEPARATOR); + String exceptionMsg = exception.getMessage(); + if (exceptionMsg.length() > EMPTY_MESSAGE) { + System.out.println(ERROR_MESSAGE_LABEL); + printMultiLineMessage(exceptionMsg); + System.out.println(EXTRA_MESSAGE_LABEL); + printMultiLineMessage(additionalMessage); + } + System.out.println(ALERT_SEPARATOR); + } + + /** + * Print output message with length > 0.
    + *
    + * 0 or more lines of messages are accepted, but lineSeparator() must be added + * if you wish to have certain message to start on a new line.
    + * Can override to accommodate to other customised error messages. + * + * @param message The exception being thrown in the program + */ + public void printOutputMessage(String message) { + printSeparator(); + if (message.length() > EMPTY_MESSAGE) { + printMultiLineMessage(message); + } + printSeparator(); + } + + //@@author nichyjt + + /** + * Prints a user-friendly cursor with the name of the feature the user is currently in. + *

    + * Cursor format: ([featureName]):~$ + * + * @param cursorName name of the feature to be tagged to the cursor + */ + public void setCursorName(String cursorName) { + assert cursorName != null && cursorName.length() > 0 : ERROR_EMPTY_STRING; + cursor = String.format(CURSOR_CARET, cursorName); + } + + /** + * Utility function to print the cursor to screen + */ + public void printCursor() { + System.out.print(cursor); + } + + /** + * Utility function to print a newline + */ + public void printNewline() { + System.out.println(System.lineSeparator()); + } + + /** + * Utility function to get the scanner belonging to TextUi. + * + * @return Scanner tagged to this instance of TextUi + */ + public Scanner getScanner() { + return scanner; + } + +} + diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/wellnus/WellNusTest.java similarity index 82% rename from src/test/java/seedu/duke/DukeTest.java rename to src/test/java/wellnus/WellNusTest.java index 2dda5fd651..0dae076cc4 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/wellnus/WellNusTest.java @@ -1,10 +1,10 @@ -package seedu.duke; +package wellnus; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -class DukeTest { +class WellNusTest { @Test public void sampleTest() { assertTrue(true); diff --git a/src/test/java/wellnus/atomichabit/AtomicHabitTest.java b/src/test/java/wellnus/atomichabit/AtomicHabitTest.java new file mode 100644 index 0000000000..20fe7f14f2 --- /dev/null +++ b/src/test/java/wellnus/atomichabit/AtomicHabitTest.java @@ -0,0 +1,335 @@ +package wellnus.atomichabit; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.HashMap; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import wellnus.atomichabit.command.AddCommand; +import wellnus.atomichabit.command.DeleteCommand; +import wellnus.atomichabit.command.ListCommand; +import wellnus.atomichabit.command.UpdateCommand; +import wellnus.atomichabit.feature.AtomicHabitList; +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.atomichabit.feature.AtomicHabitUi; +import wellnus.command.Command; +import wellnus.command.CommandParser; +import wellnus.exception.AtomicHabitException; +import wellnus.exception.WellNusException; +import wellnus.gamification.util.GamificationData; + +public class AtomicHabitTest { + private static final String ADD_HABIT_COMMAND = "add"; + private static final String UPDATE_HABIT_COMMAND = "update"; + private static final String DELETE_HABIT_COMMAND = "delete"; + private static final String LIST_HABIT_COMMAND = "list"; + private final AtomicHabitList habitList; + private final ByteArrayOutputStream outputStreamCaptor; + private final CommandParser parser; + private final GamificationData gamificationData; + + public AtomicHabitTest() { + this.habitList = new AtomicHabitList(); + this.outputStreamCaptor = new ByteArrayOutputStream(); + this.parser = new CommandParser(); + this.gamificationData = new GamificationData(); + } + + private String getMessageFrom(String uiOutput) { + AtomicHabitUi atomicHabitUi = new AtomicHabitUi(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + atomicHabitUi.printSeparator(); + String separator = outputStream.toString().trim(); + StringBuilder resultBuilder = new StringBuilder(); + String[] outputLines = uiOutput.split(System.lineSeparator()); + for (String outputLine : outputLines) { + String trimmedOutputLine = outputLine.trim(); + if (!trimmedOutputLine.equals(separator)) { + resultBuilder.append(trimmedOutputLine).append(System.lineSeparator()); + } + } + return resultBuilder.toString().trim(); + } + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + /** + * Test AddCommand with a standard payload to check output printed. + */ + @Test + public void addHabit_checkOutput_success() throws WellNusException { + String payload = "junit test"; + String expectedOutput = "Yay! You have added a new habit:" + + System.lineSeparator() + + "'" + + payload + + "'" + + " was successfully added"; + String testCommand = String.format("%s --name %s", ADD_HABIT_COMMAND, payload); + HashMap arguments = parser.parseUserInput(testCommand); + Command command = new AddCommand(arguments, habitList); + command.execute(); + Assertions.assertEquals(expectedOutput, getMessageFrom(outputStreamCaptor.toString())); + } + + /** + * Test AddCommand to throw {@link AtomicHabitException} when an invalid command is given to the AtomicHabitManager. + */ + @Test + public void addHabit_invalidCommand_atomicHabitExceptionThrown() { + // Test false command by user + AtomicHabitManager atomicHabitManager = new AtomicHabitManager(gamificationData); + String command = "sleep"; + Assertions.assertThrows(AtomicHabitException.class, () -> { + atomicHabitManager.testInvalidCommand(command); + }, "The following is an invalid command:\n" + + command); + } + + /** + * Test AddCommand to throw {@link AtomicHabitException} when a duplicate habit is added. + * + * @throws WellNusException + */ + @Test + public void addHabit_duplicateHabit_atomicHabitExceptionThrown() throws WellNusException { + String payload = "junit test"; + String testAddCommand = String.format("%s --name %s", ADD_HABIT_COMMAND, payload + System.lineSeparator()); + HashMap arguments = parser.parseUserInput(testAddCommand); + Command addCommand = new AddCommand(arguments, habitList); + addCommand.execute(); + Assertions.assertThrows(AtomicHabitException.class, addCommand::execute); + } + + /** + * Test UpdateCommand with a standard payload and default increment to check output printed. + */ + @Test + public void updateHabit_checkOutputDefaultIncrement_success() throws WellNusException { + addHabit_checkOutput_success(); + String payload = "junit test"; + String habitIndex = "1"; + String testUpdateCommand = String.format("%s --id %s", UPDATE_HABIT_COMMAND, habitIndex) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testUpdateCommand); + Command updateCommand = new UpdateCommand(arguments, habitList, gamificationData); + String expectedUpdateHabitOutput = "The following habit has been incremented! Keep up the good work!" + + System.lineSeparator() + + habitIndex + "." + payload + " " + "[1]"; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + updateCommand.execute(); + Assertions.assertEquals(expectedUpdateHabitOutput, getMessageFrom(outputStream.toString())); + } + + /** + * Test UpdateCommand with a standard payload and user-inputted increment to check output printed. + */ + @Test + public void updateHabit_checkOutputUserInputIncrement_success() throws WellNusException { + addHabit_checkOutput_success(); + String payload = "junit test"; + String habitIndex = "1"; + String increment = "3"; + String testUpdateCommand = String.format("%s --id %s --by %s", UPDATE_HABIT_COMMAND, habitIndex, increment) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testUpdateCommand); + Command updateCommand = new UpdateCommand(arguments, habitList, gamificationData); + String expectedUpdateHabitOutput = "The following habit has been incremented! Keep up the good work!" + + System.lineSeparator() + + habitIndex + "." + payload + " " + "[3]"; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + updateCommand.execute(); + Assertions.assertEquals(expectedUpdateHabitOutput, getMessageFrom(outputStream.toString())); + } + + /** + * Test UpdateCommand to throw {@link AtomicHabitException} when a non-integer index is given to the UpdateCommand. + */ + @Test + public void updateHabit_indexNotInteger_atomicHabitExceptionThrown() throws WellNusException { + // Test false command by user + addHabit_checkOutput_success(); + String habitIndex = "a"; + String testIndexCommand = String.format("%s --id %s", UPDATE_HABIT_COMMAND, habitIndex) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testIndexCommand); + Command updateCommand = new UpdateCommand(arguments, habitList, gamificationData); + Assertions.assertThrows(AtomicHabitException.class, updateCommand::execute); + } + + /** + * Test UpdateCommand to throw {@link AtomicHabitException} when an out-of-bounds index is given + * to the UpdateCommand. + */ + @Test + public void updateHabit_indexOutOfBounds_atomicHabitExceptionThrown() throws WellNusException { + // Test false command by user + addHabit_checkOutput_success(); + String largeHabitIndex = "100000000" + System.lineSeparator(); + String negativeHabitIndex = "-100000000" + System.lineSeparator(); + String testLargeIndexCommand = String.format("%s --id %s", UPDATE_HABIT_COMMAND, largeHabitIndex) + + System.lineSeparator(); + String testNegativeIndexCommand = String.format("%s --id %s", UPDATE_HABIT_COMMAND, negativeHabitIndex) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testLargeIndexCommand); + Command updateCommandForLargeIndex = new UpdateCommand(arguments, habitList, gamificationData); + Assertions.assertThrows(AtomicHabitException.class, updateCommandForLargeIndex::execute); + + arguments = parser.parseUserInput(testNegativeIndexCommand); + Command updateCommandForNegativeIndex = new UpdateCommand(arguments, habitList, gamificationData); + Assertions.assertThrows(AtomicHabitException.class, updateCommandForNegativeIndex::execute); + } + + /** + * Test UpdateCommand to successfully decrement a habit. + * + * @throws WellNusException + */ + @Test + public void updateHabit_decrement_success() throws WellNusException { + updateHabit_checkOutputUserInputIncrement_success(); + String payload = "junit test"; + String habitIndex = "1"; + String decrement = "-3"; + String testUpdateCommand = String.format("%s --id %s --by %s", UPDATE_HABIT_COMMAND, habitIndex, decrement) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testUpdateCommand); + Command updateCommand = new UpdateCommand(arguments, habitList, gamificationData); + String expectedUpdateHabitOutput = "The following habit has been decremented." + + System.lineSeparator() + + habitIndex + "." + payload + " " + "[0]"; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + updateCommand.execute(); + Assertions.assertEquals(expectedUpdateHabitOutput, getMessageFrom(outputStream.toString())); + } + + /** + * Test UpdateCommand to throw {@link AtomicHabitException} when a non-integer decrement is given. + * + * @throws WellNusException + */ + @Test + public void updateHabit_invalidDecrement_atomicHabitExceptionThrown() throws WellNusException { + updateHabit_checkOutputUserInputIncrement_success(); + String habitIndex = "1"; + String decrement = "-100000000"; + String testUpdateCommand = String.format("%s --id %s --by %s", UPDATE_HABIT_COMMAND, habitIndex, decrement) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testUpdateCommand); + Command updateCommand = new UpdateCommand(arguments, habitList, gamificationData); + Assertions.assertThrows(AtomicHabitException.class, updateCommand::execute); + } + + /** + * Test UpdateCommand for correct output when list is empty. + * + * @throws WellNusException + */ + @Test + public void updateHabit_emptyListUnsuccessful() throws WellNusException { + String habitIndex = "1"; + String decrement = "1"; + String testUpdateCommand = String.format("%s --id %s --by %s", UPDATE_HABIT_COMMAND, habitIndex, decrement) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testUpdateCommand); + Command updateCommand = new UpdateCommand(arguments, habitList, gamificationData); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + updateCommand.execute(); + String expectedOutput = "There are no habits to update!" + + " Please `add` a habit first!"; + Assertions.assertEquals(expectedOutput, getMessageFrom(outputStream.toString())); + Assertions.assertEquals(true, habitList.getAllHabits().isEmpty()); + } + + /** + * Test DeleteCommand to successfully delete a habit and check output is printed correctly. + * + * @throws WellNusException + */ + @Test + public void delete_habitSuccess() throws WellNusException { + addHabit_checkOutput_success(); + String habitIndex = "1"; + String testDeleteCommand = String.format("%s --id %s", DELETE_HABIT_COMMAND, habitIndex) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testDeleteCommand); + Command deleteCommand = new DeleteCommand(arguments, habitList); + String expectedDeleteHabitOutput = "The following habit has been deleted:" + + System.lineSeparator() + + "junit test" + " " + "[0]" + " has been successfully deleted"; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + deleteCommand.execute(); + Assertions.assertEquals(true, habitList.getAllHabits().isEmpty()); + Assertions.assertEquals(expectedDeleteHabitOutput, getMessageFrom(outputStream.toString())); + } + + /** + * Test DeleteCommand to throw {@link AtomicHabitException} when a non-integer index is given. + * + * @throws WellNusException + */ + @Test + public void delete_invalidIndex_atomicHabitExceptionThrown() throws WellNusException { + addHabit_checkOutput_success(); + String habitIndex = "1000000000000"; + String testDeleteCommand = String.format("%s --id %s", DELETE_HABIT_COMMAND, habitIndex) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testDeleteCommand); + Command deleteCommand = new DeleteCommand(arguments, habitList); + Assertions.assertThrows(AtomicHabitException.class, deleteCommand::execute); + } + + /** + * Test DeleteCommand to throw correct output when list is empty. + * + * @throws WellNusException + */ + @Test + public void delete_emptyList_unsuccessful() throws WellNusException { + String habitIndex = "1"; + String testDeleteCommand = String.format("%s --id %s", DELETE_HABIT_COMMAND, habitIndex) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testDeleteCommand); + Command deleteCommand = new DeleteCommand(arguments, habitList); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + deleteCommand.execute(); + String expectedOutput = "There are no habits to delete!" + + " Please `add` a habit first!"; + Assertions.assertEquals(expectedOutput, getMessageFrom(outputStream.toString())); + Assertions.assertEquals(true, habitList.getAllHabits().isEmpty()); + } + + /** + * Test ListCommand to print correct output when list is empty. + * + * @throws WellNusException + */ + @Test + public void listEmptyList_successful() throws WellNusException { + String testListCommand = String.format("%s", LIST_HABIT_COMMAND) + + System.lineSeparator(); + HashMap arguments = parser.parseUserInput(testListCommand); + Command listCommand = new ListCommand(arguments, habitList); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + listCommand.execute(); + String expectedOutput = "You have no habits in your list!" + System.lineSeparator() + + "Start adding some habits by using 'add'!"; + Assertions.assertEquals(expectedOutput, getMessageFrom(outputStream.toString())); + } + + +} diff --git a/src/test/java/wellnus/command/CommandParserTest.java b/src/test/java/wellnus/command/CommandParserTest.java new file mode 100644 index 0000000000..4687656a32 --- /dev/null +++ b/src/test/java/wellnus/command/CommandParserTest.java @@ -0,0 +1,232 @@ +package wellnus.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import wellnus.exception.BadCommandException; + +//@@author nichyjt + +/** + * Test that CommandParser's public functions work as intended.[ + */ +public class CommandParserTest { + + private static final String VALID_COMMAND = "mainCommand"; + private static final String VALID_COMMAND_1 = "mainCommand payload"; + private static final String VALID_COMMAND_2 = "mainCommand --arg1 pay1"; + private static final String VALID_COMMAND_3 = "mainCommand --arg1 pay1 --arg2"; + private static final String VALID_COMMAND_4 = "mainCommand --arg1 pay1 --arg2 --arg3 pay3"; + private static final String VALID_COMMAND_5 = "mainCommand pay--load"; + private static final String VALID_COMMAND_6 = "mainCommand --argument1 payload--"; + private static final String VALID_COMMAND_7 = " mainCommand --arg--1 pay1 --arg2 pay2"; + private static final String VALID_COMMAND_SPECIAL_WHITESPACE = "\n \t mainCommand payload --argument payload1"; + private static final String INVALID_COMMAND_EMPTY_ARG = "mainCommand payload --"; + private static final String INVALID_COMMAND_EMPTY_ARG_WITH_PAYLOAD = "mainCommand payload -- payload1"; + private static final String INVALID_COMMAND_EMPTY_ARG_PADDED = "mainCommand payload -- "; + private static final String INVALID_COMMAND_REPEATED_ARGS = "foo --bar payload --bar payload2"; + private static final String INVALID_COMMAND_NO_MAIN_COMMAND = "--arg1 payload"; + private static final String INVALID_COMMAND_NO_MAIN_COMMAND_PADDED = " --arg1 payload"; + private static final String EMPTY_STRING = ""; + private static final String WHITESPACE_PAYLOAD = " \n \t "; + private static final String TARGET_COMMAND = "maincommand"; + private static final String ERROR_EXCEPTION_THROWN = "CommandParser threw exception on valid input:"; + private static final String ERROR_EXPECTED_EXCEPTION = "Expected BadCommandException to be thrown. "; + + private ArrayList getValidCommandInputs() { + ArrayList validCommands = new ArrayList<>(); + validCommands.add(VALID_COMMAND); + validCommands.add(VALID_COMMAND_1); + validCommands.add(VALID_COMMAND_2); + validCommands.add(VALID_COMMAND_3); + validCommands.add(VALID_COMMAND_4); + return validCommands; + } + + /** + * Get tricky test strings that are 'adversarial' in nature but are otherwise syntatically correct + * based on our definition of what counts as a correct input. + *

    + * These tricky inputs test for whitespace padding and delimiter-based adversarial inputs. + * These should be handled appropriately by the CommandParser. + * + * @return ArrayList of strings containing adversarial but valid commands + */ + private ArrayList getValidTrickyInputs() { + ArrayList validCommands = new ArrayList<>(); + validCommands.add(VALID_COMMAND_5); + validCommands.add(VALID_COMMAND_6); + validCommands.add(VALID_COMMAND_7); + return validCommands; + } + + /** + * Test that all valid inputs defined pass the command parser's parseUserInput + */ + @Test + public void parseUserInput_validInput() { + // The following commands should be able to pass + ArrayList validCommandInputs = getValidCommandInputs(); + + // The following tests check if adversarial inputs are processed correctly + ArrayList validTrickyInputs = getValidTrickyInputs(); + + CommandParser parser = new CommandParser(); + for (String validCommand : validCommandInputs) { + try { + parser.parseUserInput(validCommand); + } catch (BadCommandException exception) { + fail(ERROR_EXCEPTION_THROWN + exception); + } + } + for (String validCommand : validTrickyInputs) { + try { + parser.parseUserInput(validCommand); + } catch (BadCommandException exception) { + fail(ERROR_EXCEPTION_THROWN + exception); + } + } + } + + /** + * Test that exception is thrown when an empty input is given + */ + @Test + public void parseUserInput_emptyInput_exceptionThrown() { + CommandParser parser = new CommandParser(); + // Test on empty user input + String command = EMPTY_STRING; + Assertions.assertThrows(BadCommandException.class, () -> { + parser.parseUserInput(command); + }, ERROR_EXPECTED_EXCEPTION + command); + } + + /** + * Test that exception is thrown when an empty argument is given " -- ". + *

    + * Account for different variations for empty argument with whitespace padding to ensure that + * the CommandParser strips all whitespace + */ + @Test + public void parseUserInput_emptyArgument_exceptionThrown() { + CommandParser parser = new CommandParser(); + // Test on empty argument + String commandEmptyArg = INVALID_COMMAND_EMPTY_ARG; + Assertions.assertThrows(BadCommandException.class, () -> { + parser.parseUserInput(commandEmptyArg); + }, ERROR_EXPECTED_EXCEPTION + commandEmptyArg); + + // Test on empty argument with padding + String commandEmptyArgPadded = INVALID_COMMAND_EMPTY_ARG_PADDED; + Assertions.assertThrows(BadCommandException.class, () -> { + parser.parseUserInput(commandEmptyArgPadded); + }, ERROR_EXPECTED_EXCEPTION + commandEmptyArgPadded); + + // Test on empty argument, payload exists + String emptyCmdWithPayload = INVALID_COMMAND_EMPTY_ARG_WITH_PAYLOAD; + Assertions.assertThrows(BadCommandException.class, () -> { + parser.parseUserInput(emptyCmdWithPayload); + }, ERROR_EXPECTED_EXCEPTION + emptyCmdWithPayload); + } + + /** + * Test that exception is thrown when there is no main argument given + */ + @Test + public void parseUserInput_noMainArgument_exceptionThrown() { + CommandParser parser = new CommandParser(); + // Test on empty user input without padding + String command = INVALID_COMMAND_NO_MAIN_COMMAND; + Assertions.assertThrows(BadCommandException.class, () -> { + parser.parseUserInput(command); + }, ERROR_EXPECTED_EXCEPTION + command); + + // Test on empty user input with padding + String commandPadded = INVALID_COMMAND_NO_MAIN_COMMAND_PADDED; + Assertions.assertThrows(BadCommandException.class, () -> { + parser.parseUserInput(commandPadded); + }, ERROR_EXPECTED_EXCEPTION + commandPadded); + } + + /** + * Test that getting the main argument (command) from user input works + */ + @Test + public void getMainArgumentTest() { + CommandParser parser = new CommandParser(); + try { + String result1 = parser.getMainArgument(VALID_COMMAND_4); + assertEquals(TARGET_COMMAND, result1); + } catch (BadCommandException exception) { + fail(exception.getMessage()); + } + } + + /** + * Test that getMainArgument works for valid whitespace padded input + */ + @Test + public void getMainArgumentTest_paddedInput_success() { + CommandParser parser = new CommandParser(); + try { + String result1 = parser.getMainArgument(VALID_COMMAND_7); + assertEquals(TARGET_COMMAND, result1); + } catch (BadCommandException exception) { + fail(exception.getMessage()); + } + } + + /** + * Test that getMainArgument works for valid \n, \t padded input + */ + @Test + public void getMainArgumentTest_specialWhitespace_success() { + CommandParser parser = new CommandParser(); + try { + String result1 = parser.getMainArgument(VALID_COMMAND_SPECIAL_WHITESPACE); + assertEquals(TARGET_COMMAND, result1); + } catch (BadCommandException exception) { + fail(exception.getMessage()); + } + } + + /** + * Test that getMainArgument throws exception for empty input + */ + @Test + public void getMainArgument_emptyInput_throwsException() { + CommandParser parser = new CommandParser(); + assertThrows(BadCommandException.class, () -> { + parser.getMainArgument(EMPTY_STRING); + }, ERROR_EXPECTED_EXCEPTION); + } + + /** + * Test that getMainArgument throws exception for whitespace-only input + */ + @Test + public void getMainArgument_whiteSpacedInput_throwsException() { + CommandParser parser = new CommandParser(); + assertThrows(BadCommandException.class, () -> { + parser.getMainArgument(WHITESPACE_PAYLOAD); + }, ERROR_EXPECTED_EXCEPTION); + } + + /** + * Test that repeated arguments throw an exception + */ + @Test + public void parseUserInput_repeatedArgument_throwsException() { + CommandParser parser = new CommandParser(); + assertThrows(BadCommandException.class, () -> { + parser.parseUserInput(INVALID_COMMAND_REPEATED_ARGS); + }, ERROR_EXPECTED_EXCEPTION); + } + +} diff --git a/src/test/java/wellnus/common/MainManagerTest.java b/src/test/java/wellnus/common/MainManagerTest.java new file mode 100644 index 0000000000..f85887f1d6 --- /dev/null +++ b/src/test/java/wellnus/common/MainManagerTest.java @@ -0,0 +1,137 @@ +package wellnus.common; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import wellnus.atomichabit.feature.AtomicHabitManager; +import wellnus.command.Command; +import wellnus.command.ExitCommand; +import wellnus.command.HelpCommand; +import wellnus.exception.BadCommandException; +import wellnus.focus.feature.FocusManager; +import wellnus.gamification.GamificationManager; +import wellnus.manager.Manager; +import wellnus.reflection.feature.ReflectionManager; + +/** + * Tests the important behaviours of MainManager to ensure + * the class is functioning as intended and expected. + */ +public class MainManagerTest { + private static final String FT_KEYWORD = "ft"; + private static final String GAMIF_KEYWORD = "gamif"; + private static final String HB_KEYWORD = "hb"; + private static final String REFLECT_KEYWORD = "reflect"; + + /** + * Checks that the 'exit' command of MainManager is recognised + * correctly. + */ + @Test + public void readCommand_exitWellNus_success() { + Command exitCommand = null; + try { + exitCommand = new MainManager().getMainCommandFor(MainManager.EXIT_COMMAND_KEYWORD); + } catch (BadCommandException badCommandException) { + fail(); + } + assertNotNull(exitCommand); + assertTrue(exitCommand instanceof ExitCommand); + } + + /** + * Checks that the 'help' command of MainManager is recognised + * correctly. + */ + @Test + public void readCommand_helpWellnus_success() { + Command helpCommand = null; + try { + helpCommand = new MainManager().getMainCommandFor(MainManager.HELP_COMMAND_KEYWORD); + } catch (BadCommandException badCommandException) { + fail(); + } + assertNotNull(helpCommand); + assertTrue(helpCommand instanceof HelpCommand); + } + + /** + * Checks that the 'hb' keyword is recognised by MainManager correctly. + */ + @Test + public void readCommand_hbCommand_success() { + MainManager mainManager = new MainManager(); + mainManager.setSupportedFeatureManagers(); + Optional hbManager = mainManager.getManagerFor(HB_KEYWORD); + if (hbManager.isEmpty()) { + fail(); + } + if (!(hbManager.get() instanceof AtomicHabitManager)) { + fail(); + } + } + + /** + * Checks that the 'ft' keyword is recognised by MainManager correctly. + */ + @Test + public void readCommand_ftCommand_success() { + MainManager mainManager = new MainManager(); + mainManager.setSupportedFeatureManagers(); + Optional ftManager = mainManager.getManagerFor(FT_KEYWORD); + if (ftManager.isEmpty()) { + fail(); + } + if (!(ftManager.get() instanceof FocusManager)) { + fail(); + } + } + + /** + * Checks that the 'gamif' keyword is recognised by MainManager correctly. + */ + @Test + public void readCommand_gamifCommand_success() { + MainManager mainManager = new MainManager(); + mainManager.setSupportedFeatureManagers(); + Optional gamifManager = mainManager.getManagerFor(GAMIF_KEYWORD); + if (gamifManager.isEmpty()) { + fail(); + } + if (!(gamifManager.get() instanceof GamificationManager)) { + fail(); + } + } + + /** + * Checks that the 'reflect' keyword is recognised by MainManager correctly. + */ + @Test + public void readCommand_reflectCommand_success() { + MainManager mainManager = new MainManager(); + mainManager.setSupportedFeatureManagers(); + Optional reflectManager = mainManager.getManagerFor(REFLECT_KEYWORD); + if (reflectManager.isEmpty()) { + fail(); + } + if (!(reflectManager.get() instanceof ReflectionManager)) { + fail(); + } + } + + /** + * Checks that MainManager can detect unrecognised keywords/commands + * successfully and throw the correct Exception. + */ + @Test + public void readCommand_unrecognisedFeature_exceptionThrown() { + String unrecognisedFeature = "test hello world"; + assertThrows(BadCommandException.class, () -> new MainManager().getMainCommandFor(unrecognisedFeature)); + } +} diff --git a/src/test/java/wellnus/focus/FocusTest.java b/src/test/java/wellnus/focus/FocusTest.java new file mode 100644 index 0000000000..b626f224e8 --- /dev/null +++ b/src/test/java/wellnus/focus/FocusTest.java @@ -0,0 +1,83 @@ +package wellnus.focus; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.HashMap; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; +import wellnus.exception.WellNusException; +import wellnus.focus.command.StartCommand; +import wellnus.focus.feature.FocusManager; +import wellnus.focus.feature.Session; +import wellnus.ui.TextUi; + + +public class FocusTest { + private static final String START_COMMAND = "start"; + private final CommandParser parser; + private final ByteArrayOutputStream outputStreamCaptor; + private final Session session; + + public FocusTest() { + this.session = new Session(); + this.parser = new CommandParser(); + this.outputStreamCaptor = new ByteArrayOutputStream(); + } + + private String getMessageFrom(String uiOutput) { + TextUi textUi = new TextUi(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + textUi.printSeparator(); + String separator = outputStream.toString().trim(); + StringBuilder resultBuilder = new StringBuilder(); + String[] outputLines = uiOutput.split(System.lineSeparator()); + for (String outputLine : outputLines) { + String trimmedOutputLine = outputLine.trim(); + if (!trimmedOutputLine.equals(separator)) { + resultBuilder.append(trimmedOutputLine).append(System.lineSeparator()); + } + } + return resultBuilder.toString().trim(); + } + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + + /** + * Test whether start command starts the timer program. + * Stops program immediately after. + * + * @throws BadCommandException + */ + @Test + void startTimer_checkResult_success() throws WellNusException { + HashMap arguments = parser.parseUserInput(START_COMMAND); + StartCommand startCommand = new StartCommand(arguments, session); + startCommand.execute(); + Assertions.assertEquals(true, session.getSession().get(0).getIsRunning()); + session.getSession().get(0).setStop(); + } + + /** + * Test FocusManager to throw {@link BadCommandException} when an invalid command is given to the FocusManager + */ + @Test + public void startTimer_invalidCommand_badCommandExceptionThrown() { + // Test false command by user + FocusManager focusManager = new FocusManager(); + String command = "wrong"; + Assertions.assertThrows(BadCommandException.class, () -> { + focusManager.testInvalidCommand(command); + }); + } +} + diff --git a/src/test/java/wellnus/focus/command/ConfigCommandTest.java b/src/test/java/wellnus/focus/command/ConfigCommandTest.java new file mode 100644 index 0000000000..90353060bc --- /dev/null +++ b/src/test/java/wellnus/focus/command/ConfigCommandTest.java @@ -0,0 +1,361 @@ +package wellnus.focus.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import wellnus.exception.WellNusException; +import wellnus.focus.feature.FocusUi; +import wellnus.focus.feature.Session; + +//@@author nichyjt + +/** + * Test that ConfigCommand's public functions work as intended. + *

    + * Only execute() is called for testing rather than the other public/protected method calls + * as it covers almost all the main logic and branches. + */ +public class ConfigCommandTest { + private static final String COMMAND_KEYWORD = "config"; + private static final String EMPTY_STRING = ""; + private static final String EXPECTED_ERROR_MAX_MINS = "" + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!" + + System.lineSeparator() + + "Error Message:" + + System.lineSeparator() + + "Invalid minutes payload given in 'config', the maximum time you can set is 60!" + + System.lineSeparator() + + "Note:" + + System.lineSeparator() + + "config command usage: config " + + "[--cycle number] [--work minutes] [--break minutes] [--longbreak minutes]" + + System.lineSeparator() + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!"; + + private static final String EXPECTED_ERROR_MIN_MINS = "" + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!" + + System.lineSeparator() + + "Error Message:" + + System.lineSeparator() + + "Invalid minutes payload given in 'config', the minimum time you can set is 1!" + + System.lineSeparator() + + "Note:" + + System.lineSeparator() + + "config command usage: config " + + "[--cycle number] [--work minutes] [--break minutes] [--longbreak minutes]" + + System.lineSeparator() + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!"; + private static final String EXPECTED_ERROR_MAX_CYCLE = "" + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!" + + System.lineSeparator() + + "Error Message:" + + System.lineSeparator() + + "Invalid cycle payload given in 'config', the maximum cycles you can set is 5!" + + System.lineSeparator() + + "Note:" + + System.lineSeparator() + + "config command usage: config " + + "[--cycle number] [--work minutes] [--break minutes] [--longbreak minutes]" + + System.lineSeparator() + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!"; + + private static final String EXPECTED_ERROR_MIN_CYCLE = "" + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!" + + System.lineSeparator() + + "Error Message:" + + System.lineSeparator() + + "Invalid cycle payload given in 'config', the minimum cycles you can set is 2!" + + System.lineSeparator() + + "Note:" + + System.lineSeparator() + + "config command usage: config " + + "[--cycle number] [--work minutes] [--break minutes] [--longbreak minutes]" + + System.lineSeparator() + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!"; + + private static final String EXPECTED_ERROR_INVALID_PAYLOAD = "" + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!" + + System.lineSeparator() + + "Error Message:" + + System.lineSeparator() + + "Invalid payload given in 'config', expected a valid integer!" + + System.lineSeparator() + + "Note:" + + System.lineSeparator() + + "config command usage: config " + + "[--cycle number] [--work minutes] [--break minutes] [--longbreak minutes]" + + System.lineSeparator() + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!"; + private static final String EXPECTED_ERROR_INVALID_ARGS = "" + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!" + + System.lineSeparator() + + "Error Message:" + + System.lineSeparator() + + "Invalid arguments given to 'config'!" + + System.lineSeparator() + + "Note:" + + System.lineSeparator() + + "config command usage: config [--cycle number] [--work minutes] [--break minutes] [--longbreak minutes]" + + System.lineSeparator() + + "!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!!"; + private static final String ERROR_EXPECTED_PASS = "Expected execution to pass but failed!"; + private static final String ERROR_UNEXPECTED_EXCEPTION = "No exception expected to throw!"; + private static final String VALID_CYCLE = "3"; + private static final int VALID_CYCLE_INT = 3; + private static final String MIN_CYCLE = "2"; + private static final int MIN_CYCLE_INT = 2; + private static final String MAX_CYCLE = "5"; + private static final int MAX_CYCLE_INT = 5; + private static final String DEFAULT_TIME = "1"; + private static final int DEFAULT_TIME_INT = 1; + private static final String VALID_TIME = "10"; + private static final int VALID_TIME_INT = 10; + private static final String VALID_TIME_1 = "15"; + private static final int VALID_TIME_1_INT = 15; + private static final String VALID_TIME_2 = "20"; + private static final int VALID_TIME_2_INT = 20; + private static final String VALID_TIME_MAX = "60"; + private static final int VALID_TIME_MAX_INT = 60; + private static final String INVALID_TIME_NEGATIVE = "-5"; + private static final String INVALID_TIME_MAX = "61"; + private static final String INVALID_CYCLE_MAX = "10"; + private static final String INVALID_CYCLE_NEGATIVE = "-5"; + private static final String INVALID_PAYLOAD = "foo"; + private static final String INVALID_PAYLOAD_1 = "bar"; + + private String getMessageFrom(String uiOutput) { + FocusUi ui = new FocusUi(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + ui.printSeparator(); + String separator = outputStream.toString().trim(); + StringBuilder resultBuilder = new StringBuilder(); + String[] outputLines = uiOutput.split(System.lineSeparator()); + for (String outputLine : outputLines) { + String trimmedOutputLine = outputLine.trim(); + if (!trimmedOutputLine.equals(separator)) { + resultBuilder.append(trimmedOutputLine).append(System.lineSeparator()); + } + } + return resultBuilder.toString().trim(); + } + + private boolean isSessionCorrectlyUpdated(Session session, int cycle, int work, int brk, int longbrk) { + if (session.getWork() != work) { + return false; + } + if (session.getBrk() != brk || session.getLongBrk() != longbrk) { + return false; + } + return session.getCycle() == cycle; + } + + private HashMap generateArguments(String cycle, String work, String brk, String longbrk) { + HashMap argumentPayload = new HashMap<>(); + argumentPayload.put(COMMAND_KEYWORD, EMPTY_STRING); + if (cycle != null) { + argumentPayload.put(ConfigCommand.ARGUMENT_CYCLE, cycle); + } + if (work != null) { + argumentPayload.put(ConfigCommand.ARGUMENT_WORK, work); + } + if (brk != null) { + argumentPayload.put(ConfigCommand.ARGUMENT_BREAK, brk); + } + if (longbrk != null) { + argumentPayload.put(ConfigCommand.ARGUMENT_LONG_BREAK, longbrk); + } + return argumentPayload; + } + + /** + * Ensure that the command works with valid arguments which may not include all arguments + */ + @Test + public void executeTest_success() { + ConfigCommand command; + Session session = new Session(); + // Test with missing arguments + HashMap argumentPayload = generateArguments(VALID_CYCLE, VALID_TIME, null, null); + command = new ConfigCommand(argumentPayload, session); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_EXPECTED_PASS); + } + assertTrue(isSessionCorrectlyUpdated(session, VALID_CYCLE_INT, VALID_TIME_INT, + DEFAULT_TIME_INT, DEFAULT_TIME_INT)); + + // Test with numbers within range + argumentPayload = generateArguments(VALID_CYCLE, VALID_TIME_2, VALID_TIME_1, VALID_TIME_2); + command = new ConfigCommand(argumentPayload, session); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_EXPECTED_PASS); + } + assertTrue(isSessionCorrectlyUpdated(session, VALID_CYCLE_INT, VALID_TIME_2_INT, + VALID_TIME_1_INT, VALID_TIME_2_INT)); + } + + /** + * Ensure that at extreme valid values, executes still works + */ + @Test + public void executeTest_minMaxRanges_success() { + ConfigCommand command; + Session session = new Session(); + // Test with edge values (max accepted values) + HashMap argumentPayload = generateArguments(MAX_CYCLE, + VALID_TIME_MAX, VALID_TIME_MAX, VALID_TIME_MAX); + command = new ConfigCommand(argumentPayload, session); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_EXPECTED_PASS); + } + assertTrue(isSessionCorrectlyUpdated(session, MAX_CYCLE_INT, + VALID_TIME_MAX_INT, VALID_TIME_MAX_INT, VALID_TIME_MAX_INT)); + + // Test with edge values (min accepted values) + argumentPayload = generateArguments(MIN_CYCLE, DEFAULT_TIME, DEFAULT_TIME, DEFAULT_TIME); + command = new ConfigCommand(argumentPayload, session); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_EXPECTED_PASS); + } + assertTrue(isSessionCorrectlyUpdated(session, MIN_CYCLE_INT, DEFAULT_TIME_INT, + DEFAULT_TIME_INT, DEFAULT_TIME_INT)); + } + + /** + * Ensure that negative numbers cause an error to be thrown + */ + @Test + public void executeTest_negativeNumbers_exceptionThrown() { + ConfigCommand command; + Session session = new Session(); + + // Test with negative time values + HashMap argumentPayload = generateArguments(MAX_CYCLE, VALID_TIME, + INVALID_TIME_NEGATIVE, VALID_TIME_2); + command = new ConfigCommand(argumentPayload, session); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_UNEXPECTED_EXCEPTION); + } + assertEquals(EXPECTED_ERROR_MIN_MINS, getMessageFrom(outputStream.toString())); + // Test with negative cycle values + argumentPayload = generateArguments(INVALID_CYCLE_NEGATIVE, VALID_TIME, + VALID_TIME, VALID_TIME); + command = new ConfigCommand(argumentPayload, session); + outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_UNEXPECTED_EXCEPTION); + } + assertEquals(EXPECTED_ERROR_MIN_CYCLE, getMessageFrom(outputStream.toString())); + } + + /** + * Ensure that large numbers (in config's context) cause an error to be thrown + */ + @Test + public void executeTest_largeNumbers_exceptionThrown() { + ConfigCommand command; + Session session = new Session(); + // Test with large time values + HashMap argumentPayload = generateArguments(MAX_CYCLE, INVALID_TIME_MAX, + VALID_TIME, VALID_TIME_2); + command = new ConfigCommand(argumentPayload, session); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_UNEXPECTED_EXCEPTION); + } + assertEquals(EXPECTED_ERROR_MAX_MINS, getMessageFrom(outputStream.toString())); + + // Test with large cycle values + outputStream = new ByteArrayOutputStream(); + argumentPayload = generateArguments(INVALID_CYCLE_MAX, VALID_TIME, VALID_TIME, VALID_TIME_2); + command = new ConfigCommand(argumentPayload, session); + System.setOut(new PrintStream(outputStream)); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_UNEXPECTED_EXCEPTION); + } + assertEquals(EXPECTED_ERROR_MAX_CYCLE, getMessageFrom(outputStream.toString())); + } + + /** + * Ensure that NaN values are correctly handled + */ + @Test + public void executeTest_notANumber_exceptionThrown() { + ConfigCommand command; + Session session = new Session(); + // Test with NaN time value + HashMap argumentPayload = generateArguments(MAX_CYCLE, INVALID_PAYLOAD, + VALID_TIME, VALID_TIME_2); + command = new ConfigCommand(argumentPayload, session); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_UNEXPECTED_EXCEPTION); + } + assertEquals(EXPECTED_ERROR_INVALID_PAYLOAD, getMessageFrom(outputStream.toString())); + + // Test with NaN cycle value + argumentPayload = generateArguments(INVALID_PAYLOAD_1, VALID_TIME, + VALID_TIME, VALID_TIME_2); + command = new ConfigCommand(argumentPayload, session); + outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_UNEXPECTED_EXCEPTION); + } + assertEquals(EXPECTED_ERROR_INVALID_PAYLOAD, getMessageFrom(outputStream.toString())); + } + + /** + * Ensure that length of argument errors are caught (too few & too many) + */ + @Test + public void executeTest_invalidArguments_exceptionThrown() { + ConfigCommand command; + Session session = new Session(); + // Test with too many arguments + HashMap argumentPayload = generateArguments(MAX_CYCLE, + VALID_TIME, VALID_TIME, VALID_TIME_2); + argumentPayload.put(INVALID_PAYLOAD, INVALID_PAYLOAD_1); + command = new ConfigCommand(argumentPayload, session); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + try { + command.execute(); + } catch (WellNusException exception) { + fail(ERROR_UNEXPECTED_EXCEPTION); + } + assertEquals(EXPECTED_ERROR_INVALID_ARGS, getMessageFrom(outputStream.toString())); + } +} diff --git a/src/test/java/wellnus/gamification/util/GamificationDataTest.java b/src/test/java/wellnus/gamification/util/GamificationDataTest.java new file mode 100644 index 0000000000..519891889c --- /dev/null +++ b/src/test/java/wellnus/gamification/util/GamificationDataTest.java @@ -0,0 +1,138 @@ +package wellnus.gamification.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +import wellnus.exception.StorageException; + + +public class GamificationDataTest { + private static final int ADD_XP_SUCCESS_AMOUNT = 5; + private static final int ADD_XP_EXCEPTION_AMOUNT = -1; + private static final int LEVEL_UP_HOW_MANY_LEVELS = 1; + private static final int LEVEL_UP_SUCCESS_AMOUNT = GamificationData.POINTS_PER_LEVEL; + private static final int MINUS_XP_SUCCESS_AMOUNT = 2; + private static final int MINUS_XP_EXCEPTION_AMOUNT = -1; + private static final String UNEXPECTED_EXCEPTION_MESSAGE = "Not expecting '%s' when testing %s."; + + /** + * Check that addXp() increments XP by given amount of XP correctly. + * @see GamificationData#addXp(int) + */ + @Test + public void addXp_validIncrease_success() { + GamificationData gamificationData = new GamificationData(); + try { + gamificationData.addXp(ADD_XP_SUCCESS_AMOUNT); + } catch (StorageException storageException) { + String exceptionName = "StorageException"; + String testCase = "addXp() with valid input"; + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, exceptionName, testCase)); + } + assertEquals(gamificationData.getTotalXp(), ADD_XP_SUCCESS_AMOUNT); + } + + /** + * Check that addXp() performs input validation and throws the correct + * assertion when given invalid input. + * @see GamificationData#addXp(int) + */ + @Test + public void addXp_negativeIncrement_exceptionThrown() { + GamificationData gamificationData = new GamificationData(); + assertThrows(AssertionError.class, () -> gamificationData.addXp(ADD_XP_EXCEPTION_AMOUNT)); + } + + /** + * Check that addXp() correctly levels up when incremented by + * sufficient amount of XP points. + * @see GamificationData#addXp(int) + */ + @Test + public void addXp_levelUp_success() { + GamificationData gamificationData = new GamificationData(); + boolean isLevelUp = false; + try { + isLevelUp = gamificationData.addXp(LEVEL_UP_SUCCESS_AMOUNT); + } catch (StorageException storageException) { + String exceptionName = "StorageException"; + String testCase = "addXp() level up with valid input"; + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, exceptionName, testCase)); + } + assertEquals(gamificationData.getXpLevel(), LEVEL_UP_HOW_MANY_LEVELS); + boolean levelledUp = true; + assertEquals(isLevelUp, levelledUp); + } + + /** + * Check that getXpForCurrentLevelOnly() calculates the correct amount + * of remaining XP for the current level. + * @see GamificationData#getXpForCurrentLevelOnly() + */ + @Test + public void getXpForCurrentLevelOnly_remainingXp_success() { + GamificationData gamificationData = new GamificationData(); + int remainingXp = 2; + try { + gamificationData.addXp(GamificationData.POINTS_PER_LEVEL + remainingXp); + } catch (StorageException storageException) { + String exceptionName = "StorageException"; + String testCase = "getXpForCurrentLevelOnly() with valid input"; + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, exceptionName, testCase)); + } + assertEquals(gamificationData.getXpForCurrentLevelOnly(), remainingXp); + } + + /** + * Check that getXpToReachNextLevel() calculates the correct amount of + * XP to level up when the total XP is less than one XP level. + * @see GamificationData#getXpToReachNextLevel() + */ + @Test + public void getXpToReachNextLevel_lessThanOneLevel_success() { + GamificationData gamificationData = new GamificationData(); + int testXpPoints = 4; + try { + gamificationData.addXp(testXpPoints); + } catch (StorageException storageException) { + String exceptionName = "StorageException"; + String testCase = "getXpToReachNextLevel() with valid input"; + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, exceptionName, testCase)); + } + int xpToReachNextLevel = GamificationData.POINTS_PER_LEVEL - testXpPoints; + assertEquals(gamificationData.getXpToReachNextLevel(), xpToReachNextLevel); + } + + /** + * Check that minusXp() correctly decrements XP points by given amount. + * @see GamificationData#minusXp(int) + */ + @Test + public void minusXp_validDecrease_success() { + GamificationData gamificationData = new GamificationData(); + try { + gamificationData.addXp(ADD_XP_SUCCESS_AMOUNT); + gamificationData.minusXp(MINUS_XP_SUCCESS_AMOUNT); + } catch (StorageException storageException) { + String exceptionName = "StorageException"; + String testCase = "minusXp() level up with valid input"; + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, exceptionName, testCase)); + } + int expectedRemaining = ADD_XP_SUCCESS_AMOUNT - MINUS_XP_SUCCESS_AMOUNT; + assertEquals(gamificationData.getTotalXp(), expectedRemaining); + } + + /** + * Check that minusXp() performs input validation and throws an + * assertion when given invalid input. + * @see GamificationData#minusXp(int) + */ + @Test + public void minusXp_negativeDecrement_exceptionThrown() { + GamificationData gamificationData = new GamificationData(); + assertThrows(AssertionError.class, () -> gamificationData.minusXp(MINUS_XP_EXCEPTION_AMOUNT)); + } +} diff --git a/src/test/java/wellnus/reflection/command/FavoriteCommandTest.java b/src/test/java/wellnus/reflection/command/FavoriteCommandTest.java new file mode 100644 index 0000000000..48fdf6e0bd --- /dev/null +++ b/src/test/java/wellnus/reflection/command/FavoriteCommandTest.java @@ -0,0 +1,65 @@ +package wellnus.reflection.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; + +import org.junit.jupiter.api.Test; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; +import wellnus.reflection.feature.QuestionList; + +// @@author wenxin-c +/** + * Class to test different tests for `FavoriteCommand` Class utilising JUnit tests. + * Test cases will involve expected outputs and correct exception handling. + */ +class FavoriteCommandTest { + private static final String LIKE_COMMAND = "like 1"; + private static final int MIN_QUESTION_LENGTH = 3; + private static final boolean IS_CORRECT_LENGTH = true; + private static final Integer[] ARR_INDEXES = { 5, 6, 7, 8, 1}; + private static final HashSet RANDOM_INDEXES = new HashSet<>(Arrays.asList(ARR_INDEXES)); + private static final QuestionList QUESTION_LIST = new QuestionList(); + private static final String FAV_COMMAND_WRONG_PAYLOAD = "fav test"; + private static final String FAV_COMMAND_WRONG_ARGUMENT = "fav --test"; + + /** + * Test whether `fav` command is validated properly.
    + * 'fav' without any payload and arguments, otherwise throw exception. + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void validateCommand_getCommand_expectException() throws BadCommandException { + CommandParser commandParser = new CommandParser(); + HashMap favCmdWrongPayload = commandParser.parseUserInput(FAV_COMMAND_WRONG_PAYLOAD); + FavoriteCommand favWrongPayload = new FavoriteCommand(favCmdWrongPayload, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> favWrongPayload.validateCommand(favCmdWrongPayload)); + HashMap favCmdWrongArgument = commandParser.parseUserInput(FAV_COMMAND_WRONG_ARGUMENT); + FavoriteCommand favWrongArgument = new FavoriteCommand(favCmdWrongPayload, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> favWrongArgument.validateCommand(favCmdWrongArgument)); + } + + /** + * Test whether fav list indexes are properly saved and returned.
    + * + * @throws BadCommandException If invalid command is given. + */ + @Test + void getFavQuestions_checkListLength_success() throws BadCommandException { + QuestionList questionList = new QuestionList(); + questionList.setRandomQuestionIndexes(RANDOM_INDEXES); + CommandParser commandParser = new CommandParser(); + HashMap argumentPayloadLikeCmd = commandParser.parseUserInput(LIKE_COMMAND); + LikeCommand likeCmd = new LikeCommand(argumentPayloadLikeCmd, questionList); + likeCmd.execute(); + String favQuestions = questionList.getFavQuestions(); + assertEquals(IS_CORRECT_LENGTH, favQuestions.length() >= MIN_QUESTION_LENGTH); + } +} + diff --git a/src/test/java/wellnus/reflection/command/GetCommandTest.java b/src/test/java/wellnus/reflection/command/GetCommandTest.java new file mode 100644 index 0000000000..ec26adb07d --- /dev/null +++ b/src/test/java/wellnus/reflection/command/GetCommandTest.java @@ -0,0 +1,80 @@ +package wellnus.reflection.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; +import wellnus.exception.StorageException; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectionManager; +import wellnus.reflection.feature.ReflectionQuestion; + +//@@author wenxin-c +/** + * Class to test different tests for `GetCommand` Class utilising JUnit tests. + * Test cases will involve expected outputs and correct exception handling. + */ +class GetCommandTest { + private static final int EXPECTED_ARRAY_LENGTH = 5; + private static final int EXPECTED_ARGUMENT_PAYLOAD_SIZE = 1; + private static final String GET_COMMAND = "get"; + private static final String EMPTY_PAYLOAD = ""; + private static final String GET_COMMAND_WRONG_PAYLOAD = "get test"; + private static final String GET_COMMAND_WRONG_ARGUMENT = "get --test"; + private static final QuestionList QUESTION_LIST = new QuestionList(); + + /** + * Test whether the ``get` command is properly parsed and generated.
    + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void createGetObject_checkArgumentPayload_success() throws BadCommandException { + ReflectionManager reflectManager = new ReflectionManager(); + reflectManager.setArgumentPayload(GET_COMMAND); + HashMap argumentPayload = reflectManager.getArgumentPayload(); + assertEquals(EXPECTED_ARGUMENT_PAYLOAD_SIZE, argumentPayload.size()); + assertTrue(argumentPayload.containsKey(GET_COMMAND)); + assertEquals(EMPTY_PAYLOAD, argumentPayload.get(GET_COMMAND)); + } + + /** + * Test the number of questions being generated.
    + * + * @throws BadCommandException If an invalid command is given. + * @throws StorageException If errors happen at storage. + */ + @Test + void getRandomQuestions_checkLength_expectFive() throws BadCommandException, StorageException { + CommandParser commandParser = new CommandParser(); + HashMap getCmdArgumentPayload = commandParser.parseUserInput(GET_COMMAND); + GetCommand get = new GetCommand(getCmdArgumentPayload, QUESTION_LIST); + ArrayList selectedQuestions = get.getRandomQuestions(); + assertEquals(EXPECTED_ARRAY_LENGTH, selectedQuestions.size()); + } + + /** + * Test whether `get` command is validated properly.
    + * 'get' without any payload and arguments, otherwise throw exception. + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void validateCommand_getCommand_expectException() throws BadCommandException { + CommandParser commandParser = new CommandParser(); + HashMap getCmdWrongPayload = commandParser.parseUserInput(GET_COMMAND_WRONG_PAYLOAD); + GetCommand getWrongPayload = new GetCommand(getCmdWrongPayload, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> getWrongPayload.validateCommand(getCmdWrongPayload)); + HashMap getCmdWrongArgument = commandParser.parseUserInput(GET_COMMAND_WRONG_ARGUMENT); + GetCommand getWrongArgument = new GetCommand(getCmdWrongArgument, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> getWrongArgument.validateCommand(getCmdWrongArgument)); + } +} + diff --git a/src/test/java/wellnus/reflection/command/HomeCommandTest.java b/src/test/java/wellnus/reflection/command/HomeCommandTest.java new file mode 100644 index 0000000000..361888d91e --- /dev/null +++ b/src/test/java/wellnus/reflection/command/HomeCommandTest.java @@ -0,0 +1,75 @@ +package wellnus.reflection.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; +import wellnus.reflection.feature.QuestionList; +import wellnus.reflection.feature.ReflectionManager; + +//@@author wenxin-c +/** + * Class to test different tests for `HomeCommand` Class utilising JUnit tests + * Test cases will involve expected outputs and correct exception handling + */ +class HomeCommandTest { + private static final String HOME_COMMAND = "home"; + private static final String HOME_COMMAND_WRONG_PAYLOAD = "home test"; + private static final String HOME_COMMAND_WRONG_ARGUMENT = "home --test"; + private static final String HOME_COMMAND_WITH_SPACES = " home "; + private static final boolean IS_EXIT = true; + private static final QuestionList QUESTION_LIST = new QuestionList(); + + /** + * Test whether ReturnCommand execute() method can terminate self reflection or not.
    + * Set isExit to true. + * + * @throws BadCommandException If invalid command is given. + */ + @Test + void execute_checkIsExit_expectTrue() throws BadCommandException { + ReflectionManager reflectionManager = new ReflectionManager(); + reflectionManager.setArgumentPayload(HOME_COMMAND); + HashMap returnArgumentPayload = reflectionManager.getArgumentPayload(); + HomeCommand homeCmd = new HomeCommand(returnArgumentPayload, QUESTION_LIST); + homeCmd.execute(); + assertEquals(IS_EXIT, reflectionManager.getIsExit()); + } + + /** + * Test whether `home` command is validated properly.
    + * 'home' without any payload and arguments, otherwise throw exception. + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void execute_checkWrongCmdFormat_expectException() throws BadCommandException { + CommandParser commandParser = new CommandParser(); + HashMap homeCmdWrongPayload = commandParser.parseUserInput(HOME_COMMAND_WRONG_PAYLOAD); + HomeCommand homeWrongPayload = new HomeCommand(homeCmdWrongPayload, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> homeWrongPayload.validateCommand(homeCmdWrongPayload)); + HashMap homeCmdWrongArgument = commandParser.parseUserInput(HOME_COMMAND_WRONG_ARGUMENT); + HomeCommand homeWrongArgument = new HomeCommand(homeCmdWrongArgument, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> homeWrongArgument.validateCommand(homeCmdWrongArgument)); + } + + /** + * Test whether command leading/dangling spaces will be removed properly.
    + * Command keyword whitespace should be trimmed. + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void execute_checkSpaceRemoval_success() throws BadCommandException { + ReflectionManager reflectionManager = new ReflectionManager(); + reflectionManager.setCommandType(HOME_COMMAND_WITH_SPACES); + String homeCommand = reflectionManager.getCommandType(); + assertEquals(HOME_COMMAND, homeCommand); + } +} + diff --git a/src/test/java/wellnus/reflection/command/LikeCommandTest.java b/src/test/java/wellnus/reflection/command/LikeCommandTest.java new file mode 100644 index 0000000000..5c7f72b71f --- /dev/null +++ b/src/test/java/wellnus/reflection/command/LikeCommandTest.java @@ -0,0 +1,106 @@ +package wellnus.reflection.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; +import wellnus.exception.ReflectionException; +import wellnus.reflection.feature.IndexMapper; +import wellnus.reflection.feature.QuestionList; + +// @@author wenxin-c +/** + * Class to test different tests for `LikeCommand` Class utilising JUnit tests. + * Test cases will involve expected outputs and correct exception handling. + */ +class LikeCommandTest { + private static final String LIKE_COMMAND_KEYWORD = "like"; + private static final String LIKE_COMMAND = "like 1"; + private static final String LIKE_COMMAND_MISSING_PARAM = "like"; + private static final String LIKE_COMMAND_WRONG_PARAM = "like ab"; + private static final String LIKE_COMMAND_OUT_OF_BOUND = "like 10"; + private static final int INPUT_INDEX = 2; + private static final int INITIAL_INDEX = 1; + private static final int INCREMENT_ONE = 1; + private static final int INDEX_ZERO = 0; + private static final boolean IS_ADDED = true; + private static final Integer[] ARR_INDEXES = { 5, 6, 7, 8, 1}; + private static final HashSet RANDOM_INDEXES = new HashSet<>(Arrays.asList(ARR_INDEXES)); + + /** + * Test whether `like` command is properly validated.
    + * `like` keyword with a valid integer is expected, otherwise throw exception. + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void validateLikeCommand_checkFormat_expectExceptions() throws BadCommandException { + QuestionList questionList = new QuestionList(); + CommandParser commandParser = new CommandParser(); + HashMap argumentPayloadMissingParam = commandParser.parseUserInput(LIKE_COMMAND_MISSING_PARAM); + LikeCommand likeCmdMissingParam = new LikeCommand(argumentPayloadMissingParam, questionList); + HashMap argumentPayloadWrongParam = commandParser.parseUserInput(LIKE_COMMAND_WRONG_PARAM); + LikeCommand likeCmdWrongParam = new LikeCommand(argumentPayloadWrongParam, questionList); + HashMap argumentPayloadOutBound = commandParser.parseUserInput(LIKE_COMMAND_OUT_OF_BOUND); + LikeCommand likeCmdOutBound = new LikeCommand(argumentPayloadOutBound, questionList); + assertThrows(NumberFormatException.class, ( + ) -> likeCmdMissingParam.addFavQuestion(argumentPayloadMissingParam.get(LIKE_COMMAND_KEYWORD))); + assertThrows(NumberFormatException.class, ( + ) -> likeCmdWrongParam.addFavQuestion(argumentPayloadWrongParam.get(LIKE_COMMAND_KEYWORD))); + assertThrows(ReflectionException.class, ( + ) -> likeCmdOutBound.addFavQuestion(argumentPayloadOutBound.get(LIKE_COMMAND_KEYWORD))); + } + + /** + * Test the mapping from user input to question index using HashMap.
    + */ + @Test + void addFavList_checkIndex_success() { + QuestionList questionList = new QuestionList(); + questionList.setRandomQuestionIndexes(RANDOM_INDEXES); + IndexMapper indexMapper = new IndexMapper(questionList.getRandomQuestionIndexes()); + HashMap indexQuestionMap = indexMapper.mapIndex(); + int count = INITIAL_INDEX; + int finalIndex = INITIAL_INDEX; + for (int index : RANDOM_INDEXES) { + if (count > INPUT_INDEX) { + break; + } + count += INCREMENT_ONE; + finalIndex = index; + } + int questionIndex = indexQuestionMap.get(INPUT_INDEX); + assertEquals(finalIndex, questionIndex); + } + + /** + * Test whether liked questions are successfully added into the favorite list.
    + * + * @throws BadCommandException + */ + @Test + void addFavList_checkQuestionList_success() throws BadCommandException { + QuestionList questionList = new QuestionList(); + questionList.setRandomQuestionIndexes(RANDOM_INDEXES); + CommandParser commandParser = new CommandParser(); + HashMap argumentPayloadLikeCmd = commandParser.parseUserInput(LIKE_COMMAND); + LikeCommand likeCmd = new LikeCommand(argumentPayloadLikeCmd, questionList); + likeCmd.execute(); + Set favList = questionList.getDataIndex().get(INDEX_ZERO); + for (int index : favList) { + System.out.println(index); + } + assertEquals(INCREMENT_ONE, favList.size()); + int index = Integer.parseInt(argumentPayloadLikeCmd.get(LIKE_COMMAND_KEYWORD)); + assertEquals(IS_ADDED, favList.contains(index)); + } +} + diff --git a/src/test/java/wellnus/reflection/command/PrevCommandTest.java b/src/test/java/wellnus/reflection/command/PrevCommandTest.java new file mode 100644 index 0000000000..f7e36346b1 --- /dev/null +++ b/src/test/java/wellnus/reflection/command/PrevCommandTest.java @@ -0,0 +1,39 @@ +package wellnus.reflection.command; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; +import wellnus.reflection.feature.QuestionList; + +// @@author wenxin-c +/** + * Class to test different tests for `PrevCommand` Class utilising JUnit tests. + * Test cases will involve correct exception handling. + */ +public class PrevCommandTest { + private static final String PREV_COMMAND_WRONG_PAYLOAD = "prev test"; + private static final String PREV_COMMAND_WRONG_ARGUMENT = "prev --test"; + private static final QuestionList QUESTION_LIST = new QuestionList(); + + /** + * Test whether `prev` command is validated properly.
    + * 'prev' without any payload and arguments, otherwise throw exception. + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void validateCommand_getCommand_expectException() throws BadCommandException { + CommandParser commandParser = new CommandParser(); + HashMap prevCmdWrongPayload = commandParser.parseUserInput(PREV_COMMAND_WRONG_PAYLOAD); + GetCommand prevWrongPayload = new GetCommand(prevCmdWrongPayload, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> prevWrongPayload.validateCommand(prevCmdWrongPayload)); + HashMap prevCmdWrongArgument = commandParser.parseUserInput(PREV_COMMAND_WRONG_ARGUMENT); + GetCommand prevWrongArgument = new GetCommand(prevCmdWrongArgument, QUESTION_LIST); + assertThrows(BadCommandException.class, () -> prevWrongArgument.validateCommand(prevCmdWrongArgument)); + } +} diff --git a/src/test/java/wellnus/reflection/command/UnlikeCommandTest.java b/src/test/java/wellnus/reflection/command/UnlikeCommandTest.java new file mode 100644 index 0000000000..23b69b9ee2 --- /dev/null +++ b/src/test/java/wellnus/reflection/command/UnlikeCommandTest.java @@ -0,0 +1,63 @@ +package wellnus.reflection.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import wellnus.command.CommandParser; +import wellnus.exception.BadCommandException; +import wellnus.exception.ReflectionException; +import wellnus.reflection.feature.QuestionList; + +// @@author wenxin-c +/** + * Class to test different tests for `UnlikeCommand` Class utilising JUnit tests. + * Test cases will involve expected outputs and correct exception handling. + */ +class UnlikeCommandTest { + private static final String UNLIKE_KEYWORD = "unlike"; + private static final String UNLIKE_COMMAND = "unlike 1"; + private static final String UNLIKE_CMD_OUT_BOUND_INDEX = "unlike -1"; + private static final String UNLIKE_CMD_WRONG_FORMAT = "unlike ab"; + private static final int EMPTY_LIST = 0; + private static final int INDEX_ZERO = 0; + + /** + * Test whether `unlike` command is properly validated.
    + * `unlike` keyword with a valid integer is expected, otherwise throw exception. + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void validateUnlikeCommand_differentFormats_success() throws BadCommandException { + QuestionList questionList = new QuestionList(); + CommandParser commandParser = new CommandParser(); + HashMap argumentsUnlikeCmdOutBound = commandParser.parseUserInput(UNLIKE_CMD_OUT_BOUND_INDEX); + UnlikeCommand unlikeCmd = new UnlikeCommand(argumentsUnlikeCmdOutBound, questionList); + if (questionList.hasFavQuestions()) { + assertThrows(ReflectionException.class, ( + ) -> unlikeCmd.removeFavQuestion(argumentsUnlikeCmdOutBound.get(UNLIKE_KEYWORD))); + } + HashMap argumentsUnlikeCmdWrongFormat = commandParser.parseUserInput(UNLIKE_CMD_WRONG_FORMAT); + assertThrows(NumberFormatException.class, ( + ) -> unlikeCmd.removeFavQuestion(argumentsUnlikeCmdWrongFormat.get(UNLIKE_KEYWORD))); + } + + /** + * Test whether empty fav list is caught properly when executing `unlike` command.
    + * + * @throws BadCommandException + */ + @Test + void checkFavListLength_emptyList_expectZero() throws BadCommandException { + QuestionList questionList = new QuestionList(); + CommandParser commandParser = new CommandParser(); + HashMap argumentsUnlikeCmdOutBound = commandParser.parseUserInput(UNLIKE_COMMAND); + UnlikeCommand unlikeCmd = new UnlikeCommand(argumentsUnlikeCmdOutBound, questionList); + unlikeCmd.execute(); + assertEquals(EMPTY_LIST, questionList.getDataIndex().get(INDEX_ZERO).size()); + } +} diff --git a/src/test/java/wellnus/reflection/feature/QuestionListTest.java b/src/test/java/wellnus/reflection/feature/QuestionListTest.java new file mode 100644 index 0000000000..119d1f57aa --- /dev/null +++ b/src/test/java/wellnus/reflection/feature/QuestionListTest.java @@ -0,0 +1,29 @@ +package wellnus.reflection.feature; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +// @@author wenxin-c +/** + * Class to test different tests for `QuestionList` Class utilising JUnit tests. + * Test cases will involve expected outputs. + */ +class QuestionListTest { + private static final int FULL_ARRAY_LENGTH = 10; + + /** + * Test the correct number of reflection questions are loaded into the list + * when `QuestionList` object is constructed.
    + * Expect 10 questions. + */ + @Test + void setUpQuestions_checkArrayLength_success() { + QuestionList questionList = new QuestionList(); + ArrayList questions = questionList.getAllQuestions(); + int fullArrayLength = questions.size(); + assertEquals(FULL_ARRAY_LENGTH, fullArrayLength); + } +} + diff --git a/src/test/java/wellnus/reflection/feature/RandomNumberGeneratorTest.java b/src/test/java/wellnus/reflection/feature/RandomNumberGeneratorTest.java new file mode 100644 index 0000000000..1712207528 --- /dev/null +++ b/src/test/java/wellnus/reflection/feature/RandomNumberGeneratorTest.java @@ -0,0 +1,26 @@ +package wellnus.reflection.feature; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +/** + * Class to test different tests for `RandomNumberGenerator` Class utilising JUnit tests. + * Test cases will involve expected outputs. + */ +class RandomNumberGeneratorTest { + private static final int NUM_OF_RANDOM_NUMBERS = 5; + private static final int UPPER_BOUND = 9; + + /** + * Test whether a set of 5 integers from 0-9 are generated correctly. + */ + @Test + void generateRandomIndexes_checkSetSize_expectFive() { + RandomNumberGenerator randomNumberGenerator = new RandomNumberGenerator(UPPER_BOUND); + Set indexes = randomNumberGenerator.generateRandomNumbers(); + assertEquals(NUM_OF_RANDOM_NUMBERS, indexes.size()); + } +} diff --git a/src/test/java/wellnus/reflection/feature/ReflectionManagerTest.java b/src/test/java/wellnus/reflection/feature/ReflectionManagerTest.java new file mode 100644 index 0000000000..fdb4092116 --- /dev/null +++ b/src/test/java/wellnus/reflection/feature/ReflectionManagerTest.java @@ -0,0 +1,64 @@ +package wellnus.reflection.feature; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import wellnus.exception.BadCommandException; + +// @@author wenxin-c +/** + * Class to test different tests for `ReflectionManager` Class utilising JUnit tests. + * Test cases will involve expected outputs and correct exception handling. + */ +class ReflectionManagerTest { + private static final String EMPTY_STRING = ""; + private static final String GET_COMMAND = "get"; + private static final String INVALID_COMMAND = "test"; + private static final String SEPARATOR = " "; + + /** + * Test whether commands are validated correctly.
    + * + * @throws BadCommandException If unrecognised command is given. + */ + @Test + void execution_invalidCommand_expectException() throws BadCommandException { + ReflectionManager reflectionManager = new ReflectionManager(); + reflectionManager.setCommandType(INVALID_COMMAND); + assertThrows(BadCommandException.class, + reflectionManager::executeCommands); + } + + /** + * Test whether empty string input exception is properly thrown and caught. + */ + @Test + void setCommandType_emptyString_expectException() { + ReflectionManager reflectionManager = new ReflectionManager(); + String[] input = EMPTY_STRING.split(SEPARATOR); + System.out.println(input.length); + assertThrows(BadCommandException.class, ( + ) -> reflectionManager.setCommandType(EMPTY_STRING)); + assertThrows(BadCommandException.class, ( + ) -> reflectionManager.setArgumentPayload(EMPTY_STRING)); + } + + /** + * Test whether command argument_payload pair is properly generated.
    + * + * @throws BadCommandException If an invalid command is given. + */ + @Test + void setArgumentPayload_singleCommand_expectEmptyPayload() throws BadCommandException { + ReflectionManager reflectionManager = new ReflectionManager(); + reflectionManager.setArgumentPayload(GET_COMMAND); + HashMap argumentPayload = reflectionManager.getArgumentPayload(); + String value = argumentPayload.get(GET_COMMAND); + assertEquals(EMPTY_STRING, value); + } +} + diff --git a/src/test/java/wellnus/reflection/feature/ReflectionQuestionTest.java b/src/test/java/wellnus/reflection/feature/ReflectionQuestionTest.java new file mode 100644 index 0000000000..97ae92871e --- /dev/null +++ b/src/test/java/wellnus/reflection/feature/ReflectionQuestionTest.java @@ -0,0 +1,34 @@ +package wellnus.reflection.feature; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Class to test different tests for `ReflectionQuestion` Class utilising JUnit tests. + * Test cases will involve expected outputs. + */ +class ReflectionQuestionTest { + private static final String QUESTION = "How's today?"; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + /** + * Test whether questions will be created and printed in a proper format. + */ + @Test + void createReflectionQuestion_printString_success() { + ReflectionQuestion question = new ReflectionQuestion(QUESTION); + System.out.print(question); + assertEquals(QUESTION, outputStreamCaptor.toString().trim()); + } +} + diff --git a/src/test/java/wellnus/storage/AtomicHabitTokenizerTest.java b/src/test/java/wellnus/storage/AtomicHabitTokenizerTest.java new file mode 100644 index 0000000000..4ae9c7084c --- /dev/null +++ b/src/test/java/wellnus/storage/AtomicHabitTokenizerTest.java @@ -0,0 +1,129 @@ +package wellnus.storage; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import wellnus.atomichabit.feature.AtomicHabit; +import wellnus.exception.TokenizerException; + +/** + * This class provides tests for the AtomicHabitTokenizer class. It tests the functionality of the methods of the + * AtomicHabitTokenizer class using various inputs. + */ +public class AtomicHabitTokenizerTest { + private static final int INDEX_ZERO = 0; + private static final int INDEX_ONE = 1; + private static final String EXPECTED_TOKENIZED_HABIT_ONE = "--description foo --count 1"; + private static final String EXPECTED_TOKENIZED_HABIT_TWO = "--description bar --count 2"; + private static final String ATOMIC_HABIT_TEST_ONE_DESCRIPTION = "foo"; + private static final String ATOMIC_HABIT_TEST_TWO_DESCRIPTION = "bar"; + private static final int ATOMIC_HABIT_TEST_ONE_COUNT = 1; + private static final int ATOMIC_HABIT_TEST_TWO_COUNT = 2; + + private static final String TOKENIZED_HABIT_TEST_ONE = "--description foo --count 1"; + private static final String TOKENIZED_HABIT_TEST_TWO = "--description bar baz --count 1000"; + private static final String EXPECTED_DESCRIPTION_TEST_ONE = "foo"; + private static final String EXPECTED_DESCRIPTION_TEST_TWO = "bar baz"; + private static final int EXPECTED_COUNT_TEST_ONE = 1; + private static final int EXPECTED_COUNT_TEST_TWO = 1000; + private static final String INVALID_STRING_ONE = "foo"; + private static final String INVALID_STRING_TWO = "--description"; + private static final String INVALID_STRING_THREE = "description count"; + private static final String INVALID_STRING_FOUR = "--description --count"; + private static final String INVALID_STRING_FIVE = "--description foo --count bar"; + private static final String INVALID_STRING_SIX = "--description foo --count 1 --baz baz"; + + private ArrayList getInvalidTokenizedArrayList(String invalidString) { + ArrayList invalidTokenizedArrayList = new ArrayList<>(); + invalidTokenizedArrayList.add(invalidString); + return invalidTokenizedArrayList; + } + + /** + * Tests the {@link AtomicHabitTokenizer#tokenize(ArrayList)} method to ensure that it correctly + * tokenizes a list of {@link AtomicHabit} objects. + */ + @Test + void tokenizeHabit_checkOutput_success() { + ArrayList habitsToTokenize = new ArrayList<>(); + AtomicHabit atomicHabitTestOne = new AtomicHabit(ATOMIC_HABIT_TEST_ONE_DESCRIPTION, + ATOMIC_HABIT_TEST_ONE_COUNT); + AtomicHabit atomicHabitTestTwo = new AtomicHabit(ATOMIC_HABIT_TEST_TWO_DESCRIPTION, + ATOMIC_HABIT_TEST_TWO_COUNT); + habitsToTokenize.add(atomicHabitTestOne); + habitsToTokenize.add(atomicHabitTestTwo); + AtomicHabitTokenizer habitTokenizer = new AtomicHabitTokenizer(); + ArrayList actualTokenizedAtomicHabits = habitTokenizer.tokenize(habitsToTokenize); + Assertions.assertEquals(EXPECTED_TOKENIZED_HABIT_ONE, actualTokenizedAtomicHabits.get(INDEX_ZERO)); + Assertions.assertEquals(EXPECTED_TOKENIZED_HABIT_TWO, actualTokenizedAtomicHabits.get(INDEX_ONE)); + } + + /** + * Tests the {@link AtomicHabitTokenizer#detokenize(ArrayList)} method to ensure that it correctly + * detokenizes a list of tokenized {@link AtomicHabit} objects. + * + * @throws TokenizerException if an error occurs during tokenization. + */ + @Test + void detokenizeHabit_checkOutput_success() throws TokenizerException { + ArrayList tokenizedHabits = new ArrayList<>(); + tokenizedHabits.add(TOKENIZED_HABIT_TEST_ONE); + tokenizedHabits.add(TOKENIZED_HABIT_TEST_TWO); + AtomicHabitTokenizer habitTokenizer = new AtomicHabitTokenizer(); + ArrayList actualDetokenizedAtomicHabits = habitTokenizer.detokenize(tokenizedHabits); + assertEquals(EXPECTED_DESCRIPTION_TEST_ONE, actualDetokenizedAtomicHabits.get(INDEX_ZERO).getDescription()); + assertEquals(EXPECTED_COUNT_TEST_ONE, actualDetokenizedAtomicHabits.get(INDEX_ZERO).getCount()); + assertEquals(EXPECTED_DESCRIPTION_TEST_TWO, actualDetokenizedAtomicHabits.get(INDEX_ONE).getDescription()); + assertEquals(EXPECTED_COUNT_TEST_TWO, actualDetokenizedAtomicHabits.get(INDEX_ONE).getCount()); + } + + /** + * Tests the {@link AtomicHabitTokenizer#detokenize(ArrayList)} method to ensure that it correctly + * detokenizes an empty list of tokenized {@link AtomicHabit} objects. + * + * @throws TokenizerException if an error occurs during tokenization. + */ + @Test + void detokenizeHabit_checkOutputEmptyString_success() throws TokenizerException { + ArrayList expectedDetokenizedAtomicHabit = new ArrayList<>(); + ArrayList tokenizedHabits = new ArrayList<>(); + AtomicHabitTokenizer habitTokenizer = new AtomicHabitTokenizer(); + ArrayList actualDetokenizedAtomicHabits = habitTokenizer.detokenize(tokenizedHabits); + assertEquals(expectedDetokenizedAtomicHabit, actualDetokenizedAtomicHabits); + } + + /** + * Tests the {@link AtomicHabitTokenizer#detokenize(ArrayList)} method to ensure that it throws a + * {@link TokenizerException} when attempting to detokenize an invalid tokenized {@link AtomicHabit} + * string. + */ + @Test + void detokenizeHabit_invalidTokenizedAtomicHabitString_tokenizerExceptionThrown() { + AtomicHabitTokenizer habitTokenizer = new AtomicHabitTokenizer(); + Assertions.assertThrows(TokenizerException.class, () -> { + habitTokenizer.detokenize(getInvalidTokenizedArrayList(INVALID_STRING_ONE)); + }); + Assertions.assertThrows(TokenizerException.class, () -> { + habitTokenizer.detokenize(getInvalidTokenizedArrayList(INVALID_STRING_TWO)); + }); + + Assertions.assertThrows(TokenizerException.class, () -> { + habitTokenizer.detokenize(getInvalidTokenizedArrayList(INVALID_STRING_THREE)); + }); + + Assertions.assertThrows(TokenizerException.class, () -> { + habitTokenizer.detokenize(getInvalidTokenizedArrayList(INVALID_STRING_FOUR)); + }); + + Assertions.assertThrows(TokenizerException.class, () -> { + habitTokenizer.detokenize(getInvalidTokenizedArrayList(INVALID_STRING_FIVE)); + }); + + Assertions.assertThrows(TokenizerException.class, () -> { + habitTokenizer.detokenize(getInvalidTokenizedArrayList(INVALID_STRING_SIX)); + }); + } +} diff --git a/src/test/java/wellnus/storage/GamificationTokenizerTest.java b/src/test/java/wellnus/storage/GamificationTokenizerTest.java new file mode 100644 index 0000000000..572403685b --- /dev/null +++ b/src/test/java/wellnus/storage/GamificationTokenizerTest.java @@ -0,0 +1,163 @@ +package wellnus.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import wellnus.exception.StorageException; +import wellnus.exception.TokenizerException; +import wellnus.gamification.util.GamificationData; + +/** + * Tests the important behaviours of the GamificationTokenizer class + * to ensure it functions as intended and expected. + */ +public class GamificationTokenizerTest { + private static final int ADD_XP_AMOUNT = 5; + private static final int FIRST_ELEMENT = 0; + private static final int INITIAL_XP = 0; + private static final int NEGATIVE_XP = -1; + private static final int NO_LEVEL_XP = 5; + private static final int LEVEL_UP_XP = 10; + private static final String UNEXPECTED_EXCEPTION_MESSAGE = "TokenizerException not supposed to be thrown for valid " + + "input '%d'."; + private static final String WRONG_EXCEPTION_MESSAGE = "'%s' thrown when testing valid input '%d'. Not even the " + + "relevant Exception category."; + private static final int XP_LEVEL_ZERO = 0; + private static final int XP_LEVEL_ONE = 1; + + //@@author haoyangw + /** + * Check that GamificationTokenizer can tokenize a newly-initialised + * GamificationData correctly. + */ + @Test + public void tokenize_initialGamificationData_success() { + GamificationTokenizer gamificationTokenizer = new GamificationTokenizer(); + GamificationData gamificationData = new GamificationData(); + ArrayList testDatas = new ArrayList<>(); + testDatas.add(gamificationData); + ArrayList tokenizedDatas = gamificationTokenizer.tokenize(testDatas); + String tokenizedData = tokenizedDatas.get(FIRST_ELEMENT); + String expectedTokenizedData = INITIAL_XP + ""; + assertEquals(tokenizedData, expectedTokenizedData); + } + + /** + * Check that GamificationTokenizer can tokenize a + * GamificationData already containing some XP correctly. + */ + @Test + public void tokenize_hasXpGamificationData_success() { + GamificationTokenizer gamificationTokenizer = new GamificationTokenizer(); + GamificationData gamificationData = new GamificationData(); + try { + gamificationData.addXp(ADD_XP_AMOUNT); + } catch (StorageException storageException) { + String exceptionName = "StorageException"; + fail(String.format(WRONG_EXCEPTION_MESSAGE, exceptionName, ADD_XP_AMOUNT)); + } + ArrayList testDatas = new ArrayList<>(); + testDatas.add(gamificationData); + ArrayList tokenizedDatas = gamificationTokenizer.tokenize(testDatas); + String tokenizedData = tokenizedDatas.get(FIRST_ELEMENT); + String expectedTokenizedData = ADD_XP_AMOUNT + ""; + assertEquals(tokenizedData, expectedTokenizedData); + } + + /** + * Check that GamificationTokenizer can parse and detokenize what should be + * a default-state GamificationData's String. + */ + @Test + public void detokenize_initialGamificationData_success() { + GamificationTokenizer gamificationTokenizer = new GamificationTokenizer(); + String tokenizedData = INITIAL_XP + ""; + ArrayList tokenizedDatas = new ArrayList<>(); + tokenizedDatas.add(tokenizedData); + ArrayList detokenizedDatas = null; + try { + detokenizedDatas = gamificationTokenizer.detokenize(tokenizedDatas); + } catch (TokenizerException tokenizerException) { + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, INITIAL_XP)); + } + assertNotNull(detokenizedDatas); + GamificationData detokenizedData = detokenizedDatas.get(FIRST_ELEMENT); + assertEquals(detokenizedData.getTotalXp(), INITIAL_XP); + } + + /** + * Check that GamificationTokenizer can detokenize what should be + * a GamificationData with 0 XP levels but some XP points correctly. + */ + @Test + public void detokenize_noXpLevels_success() { + GamificationTokenizer gamificationTokenizer = new GamificationTokenizer(); + String tokenizedData = NO_LEVEL_XP + ""; + ArrayList tokenizedDatas = new ArrayList<>(); + tokenizedDatas.add(tokenizedData); + ArrayList detokenizedDatas = null; + try { + detokenizedDatas = gamificationTokenizer.detokenize(tokenizedDatas); + } catch (TokenizerException tokenizerException) { + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, NO_LEVEL_XP)); + } + assertNotNull(detokenizedDatas); + GamificationData detokenizedData = detokenizedDatas.get(FIRST_ELEMENT); + assertEquals(detokenizedData.getTotalXp(), NO_LEVEL_XP); + assertEquals(detokenizedData.getXpLevel(), XP_LEVEL_ZERO); + } + + /** + * Check that GamificationTokenizer can detokenize what should be + * a GamificationData with some XP levels correctly. + */ + @Test + public void detokenize_hasXpLevels_success() { + GamificationTokenizer gamificationTokenizer = new GamificationTokenizer(); + String tokenizedData = LEVEL_UP_XP + ""; + ArrayList tokenizedDatas = new ArrayList<>(); + tokenizedDatas.add(tokenizedData); + ArrayList detokenizedDatas = null; + try { + detokenizedDatas = gamificationTokenizer.detokenize(tokenizedDatas); + } catch (TokenizerException tokenizerException) { + fail(String.format(UNEXPECTED_EXCEPTION_MESSAGE, LEVEL_UP_XP)); + } + assertNotNull(detokenizedDatas); + GamificationData detokenizedData = detokenizedDatas.get(FIRST_ELEMENT); + assertEquals(detokenizedData.getTotalXp(), LEVEL_UP_XP); + assertEquals(detokenizedData.getXpLevel(), XP_LEVEL_ONE); + } + + /** + * Check that GamificationTokenizer can detect corrupted data + * and throw the correct TokenizerException. + */ + @Test + public void detokenize_notIntData_exceptionThrown() { + GamificationTokenizer gamificationTokenizer = new GamificationTokenizer(); + String garbageTokenizedData = "definitely not data"; + ArrayList tokenizedDatas = new ArrayList<>(); + tokenizedDatas.add(garbageTokenizedData); + assertThrows(TokenizerException.class, () -> gamificationTokenizer.detokenize(tokenizedDatas)); + } + + /** + * Check that GamificationTokenizer can detect invalid XP points read from storage + * and throw the correct TokenizerException. + */ + @Test + public void detokenize_negativeIntData_exceptionThrown() { + GamificationTokenizer gamificationTokenizer = new GamificationTokenizer(); + String garbageTokenizedData = NEGATIVE_XP + ""; + ArrayList tokenizedDatas = new ArrayList<>(); + tokenizedDatas.add(garbageTokenizedData); + assertThrows(TokenizerException.class, () -> gamificationTokenizer.detokenize(tokenizedDatas)); + } +} diff --git a/src/test/java/wellnus/storage/ReflectionTokenizerTest.java b/src/test/java/wellnus/storage/ReflectionTokenizerTest.java new file mode 100644 index 0000000000..7085dcff31 --- /dev/null +++ b/src/test/java/wellnus/storage/ReflectionTokenizerTest.java @@ -0,0 +1,114 @@ +package wellnus.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import wellnus.exception.TokenizerException; + +/** + * This class provides tests for the ReflectionTokenizer class. It tests the functionality of the methods of the + * ReflectionTokenizer class using various inputs. + */ +public class ReflectionTokenizerTest { + private static final int NUMBER_ZERO = 0; + private static final int NUMBER_ONE = 1; + private static final int NUMBER_TWO = 2; + private static final int NUMBER_THREE = 3; + private static final int NUMBER_FOUR = 4; + private static final int NUMBER_FIVE = 5; + private static final String EXPECTED_TOKENIZED_LIKE = "like:1,2"; + private static final String EXPECTED_TOKENIZED_PREV = "prev:3,4"; + private static final String EXPECTED_TOKENIZED_LIKE_EMPTY = "like:"; + private static final String EXPECTED_TOKENIZED_PREV_EMPTY = "prev:"; + private static final String TOKENIZED_LIKE_TEST = "like:1,2"; + private static final String TOKENIZED_PREV_TEST = "prev:1,2,3,4,5"; + + /** + * Tests the {@link ReflectionTokenizer#tokenize(ArrayList)} method to ensure that it correctly + * tokenizes a set of like, prev indexes. + */ + @Test + void tokenizeReflect_checkOutput_success() { + ArrayList> indexesToTokenize = new ArrayList<>(); + Set likeTestIndexes = new HashSet<>(); + likeTestIndexes.add(NUMBER_ONE); + likeTestIndexes.add(NUMBER_TWO); + Set prevTestIndexes = new HashSet<>(); + prevTestIndexes.add(NUMBER_THREE); + prevTestIndexes.add(NUMBER_FOUR); + indexesToTokenize.add(likeTestIndexes); + indexesToTokenize.add(prevTestIndexes); + ArrayList expectedTokenizedIndex = new ArrayList<>(); + expectedTokenizedIndex.add(EXPECTED_TOKENIZED_LIKE); + expectedTokenizedIndex.add(EXPECTED_TOKENIZED_PREV); + ReflectionTokenizer reflectionTokenizer = new ReflectionTokenizer(); + ArrayList actualTokenizedIndex = reflectionTokenizer.tokenize(indexesToTokenize); + Assertions.assertEquals(expectedTokenizedIndex, actualTokenizedIndex); + } + + /** + * Tests the {@link ReflectionTokenizer#tokenize(ArrayList)} method to ensure that it correctly + * tokenizes a set of like, prev indexes when it is empty. + */ + @Test + void tokenizeReflect_checkOutputEmptyIndex_success() { + ArrayList> indexesToTokenize = new ArrayList<>(); + Set likeTestIndexes = new HashSet<>(); + Set prevTestIndexes = new HashSet<>(); + indexesToTokenize.add(likeTestIndexes); + indexesToTokenize.add(prevTestIndexes); + ArrayList expectedTokenizedIndex = new ArrayList<>(); + expectedTokenizedIndex.add(EXPECTED_TOKENIZED_LIKE_EMPTY); + expectedTokenizedIndex.add(EXPECTED_TOKENIZED_PREV_EMPTY); + ReflectionTokenizer reflectionTokenizer = new ReflectionTokenizer(); + ArrayList actualTokenizedIndex = reflectionTokenizer.tokenize(indexesToTokenize); + Assertions.assertEquals(expectedTokenizedIndex, actualTokenizedIndex); + } + + /** + * Tests the {@link ReflectionTokenizer#detokenize(ArrayList)} method to ensure that it correctly + * detokenizes a list of tokenized like and prev indexes. + * + * @throws TokenizerException if an error occurs during tokenization. + */ + @Test + void detokenizeReflect_checkOutput_success() throws TokenizerException { + Set expectedDetokenizedLikes = new HashSet<>(); + expectedDetokenizedLikes.add(NUMBER_ONE); + expectedDetokenizedLikes.add(NUMBER_TWO); + Set expectedDetokenizedPrevs = new HashSet<>(); + expectedDetokenizedPrevs.add(NUMBER_ONE); + expectedDetokenizedPrevs.add(NUMBER_TWO); + expectedDetokenizedPrevs.add(NUMBER_THREE); + expectedDetokenizedPrevs.add(NUMBER_FOUR); + expectedDetokenizedPrevs.add(NUMBER_FIVE); + ArrayList stringsToDetokenize = new ArrayList<>(); + stringsToDetokenize.add(TOKENIZED_LIKE_TEST); + stringsToDetokenize.add(TOKENIZED_PREV_TEST); + ReflectionTokenizer reflectionTokenizer = new ReflectionTokenizer(); + ArrayList> actualDetokenizedIndex = reflectionTokenizer.detokenize(stringsToDetokenize); + Assertions.assertEquals(expectedDetokenizedLikes, actualDetokenizedIndex.get(NUMBER_ZERO)); + Assertions.assertEquals(expectedDetokenizedPrevs, actualDetokenizedIndex.get(NUMBER_ONE)); + } + + /** + * Tests the {@link ReflectionTokenizer#detokenize(ArrayList)} method to ensure that it correctly + * detokenizes a list of tokenized like and prev indexes when it is empty. + * + * @throws TokenizerException if an error occurs during tokenization. + */ + @Test + void detokenizeReflect_checkOutputEmptyString_success() throws TokenizerException { + Set expectedDetokenizedLikes = new HashSet<>(); + Set expectedDetokenizedPrevs = new HashSet<>(); + ArrayList stringsToDetokenize = new ArrayList<>(); + ReflectionTokenizer reflectionTokenizer = new ReflectionTokenizer(); + ArrayList> actualDetokenizedIndex = reflectionTokenizer.detokenize(stringsToDetokenize); + Assertions.assertEquals(expectedDetokenizedLikes, actualDetokenizedIndex.get(NUMBER_ZERO)); + Assertions.assertEquals(expectedDetokenizedPrevs, actualDetokenizedIndex.get(NUMBER_ONE)); + } +} diff --git a/src/test/java/wellnus/storage/StorageTest.java b/src/test/java/wellnus/storage/StorageTest.java new file mode 100644 index 0000000000..57392cb077 --- /dev/null +++ b/src/test/java/wellnus/storage/StorageTest.java @@ -0,0 +1,253 @@ +package wellnus.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.util.ArrayList; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import wellnus.exception.StorageException; + +//@@author nichyjt + +/** + * Test that Storage's public functions work as intended. + */ +public class StorageTest { + + private static final String INVALID_FILENAME = "foobar"; + private static final String ERROR_FAIL_STORAGE_INSTANCE = "Failed to create instance of storage"; + private static final String EXPECTED_EXCEPTION_FILENAME = "Expected exception to be thrown for invalid filename"; + private static final String ERROR_CLEANUP_FILE_FAIL = "Failed to cleanup file!"; + private static final String ERROR_LOAD_FAIL = "loadData failed when loading file that does not exist" + + " despite safety checks"; + private static final String ERROR_DELETE_FILE_NOT_EXIST_FAIL = "deleteFile failed on file not exist!"; + private static final String ERROR_STORAGE_FAIL_LOAD = "Storage failed to load data!"; + private static final String ERROR_STORAGE_FAIL_SAVE = "Storage failed to save data!"; + private static final String ERROR_STORAGE_FAIL_DELETE = "Failed to delete file!"; + private static final String ERROR_STORAGE_FAIL_CREATE = "Failed to create and get new file!"; + private static final String DEBUG_PAYLOAD_0 = "attr0 p0"; + private static final String DEBUG_PAYLOAD_1 = "attr1 p1 --p2 p3"; + private static final String DEBUG_PAYLOAD_2 = "attr2 --p1 p2 --p3 --p4"; + private static final String DEBUG_PAYLOAD_3 = "attr3"; + + private Storage getStorageInstance() { + + Storage storage; + try { + storage = new Storage(); + } catch (StorageException exception) { + fail(ERROR_FAIL_STORAGE_INSTANCE); + return null; + } + return storage; + } + + private ArrayList getDebugStringList() { + ArrayList stringList = new ArrayList<>(); + stringList.add(DEBUG_PAYLOAD_0); + stringList.add(DEBUG_PAYLOAD_1); + stringList.add(DEBUG_PAYLOAD_2); + stringList.add(DEBUG_PAYLOAD_3); + return stringList; + } + + private String getDebugTokenizedString() { + return DEBUG_PAYLOAD_0 + + Storage.DELIMITER + + DEBUG_PAYLOAD_1 + + Storage.DELIMITER + + DEBUG_PAYLOAD_2 + + Storage.DELIMITER + + DEBUG_PAYLOAD_3 + + Storage.DELIMITER; + } + + /** + * Test that creating and deleting a file works + */ + @Test + @Order(1) + public void createAndDeleteFile_test() { + Storage storage = getStorageInstance(); + assert storage != null; + String debugFilename = Storage.FILE_DEBUG; + // Create test + File debugFile; + try { + debugFile = storage.getFile(debugFilename); + } catch (StorageException exception) { + fail(ERROR_STORAGE_FAIL_CREATE); + return; + } + // Remove test + try { + storage.deleteFile(debugFilename); + } catch (StorageException exception) { + fail(ERROR_STORAGE_FAIL_DELETE); + return; + } + // Sanity check that file actually is deleted + assertFalse(debugFile.exists()); + } + + /** + * Test that tokenizing a list data string works + */ + @Test + @Order(2) + public void tokenizeHashmap_test() { + Storage storage = getStorageInstance(); + assert storage != null; + + ArrayList debugList = getDebugStringList(); + + String result = storage.tokenizeStringList(debugList); + String expected = getDebugTokenizedString(); + assertEquals(expected, result); + } + + /** + * Test that detokenizing data string works for a valid string + */ + @Test + @Order(3) + public void detokenizeDataString_test() { + Storage storage = getStorageInstance(); + assert storage != null; + + String dataString = getDebugTokenizedString(); + ArrayList expectedList = getDebugStringList(); + ArrayList result = storage.detokenizeDataString(dataString); + assertEquals(result, expectedList); + } + + /** + * Tests the end-to-end of saving and loading. + */ + @Test + @Order(4) + public void saveAndLoadData_test() { + Storage storage = getStorageInstance(); + assert storage != null; + + String debugFilename = Storage.FILE_DEBUG; + // Test saving logic + ArrayList debugList = getDebugStringList(); + try { + storage.saveData(getDebugStringList(), debugFilename); + } catch (StorageException exception) { + fail(ERROR_STORAGE_FAIL_SAVE); + } + // Test loading logic + ArrayList result = new ArrayList<>(); + try { + result = storage.loadData(debugFilename); + } catch (StorageException exception) { + fail(ERROR_STORAGE_FAIL_LOAD); + } + assertEquals(debugList, result); + // Cleanup file + try { + storage.deleteFile(debugFilename); + } catch (StorageException exception) { + fail(ERROR_CLEANUP_FILE_FAIL); + } + } + + /** + * Ensures that deleting a file that does not exist due to developer error does not crash WellNUS++ + */ + @Test + @Order(5) + public void deleteFile_fileNotExist_success() { + Storage storage = getStorageInstance(); + assert storage != null; + try { + storage.deleteFile(Storage.FILE_DEBUG); + } catch (StorageException exception) { + fail(ERROR_DELETE_FILE_NOT_EXIST_FAIL); + } + } + + /** + * Ensure that loading an un-instantiated file automatically creates the file as safety behaviour + */ + @Test + @Order(6) + public void loadFile_fileNotExist() { + Storage storage = getStorageInstance(); + assert storage != null; + try { + storage.loadData(Storage.FILE_DEBUG); + } catch (StorageException exception) { + fail(ERROR_LOAD_FAIL); + } + // Cleanup the debug file that was created as part of safety measures + // deleteFile must work as the above tests on deleteFile have passed + try { + storage.deleteFile(Storage.FILE_DEBUG); + } catch (StorageException exception) { + fail(ERROR_CLEANUP_FILE_FAIL); + } + } + + /** + * Test that invalid file name throws an exception on getFile + */ + @Test + @Order(7) + public void getFile_invalidFileName_exceptionThrown() { + Storage storage = getStorageInstance(); + assert storage != null; + assertThrows(StorageException.class, () -> { + storage.getFile(INVALID_FILENAME); + }, EXPECTED_EXCEPTION_FILENAME); + } + + /** + * Test that invalid file name throws an exception on saveData + */ + @Test + @Order(8) + public void saveData_invalidFileName_exceptionThrown() { + Storage storage = getStorageInstance(); + assert storage != null; + ArrayList payload = getDebugStringList(); + assertThrows(StorageException.class, () -> { + storage.saveData(payload, INVALID_FILENAME); + }, EXPECTED_EXCEPTION_FILENAME); + } + + /** + * Test that invalid file name throws an exception on loadData + */ + @Test + @Order(9) + public void loadData_invalidFileName_exceptionThrown() { + Storage storage = getStorageInstance(); + assert storage != null; + assertThrows(StorageException.class, () -> { + storage.loadData(INVALID_FILENAME); + }, EXPECTED_EXCEPTION_FILENAME); + } + + /** + * Test that invalid file name throws an exception on deleteFile + */ + @Test + @Order(10) + public void deleteFile_invalidFileName_exceptionThrown() { + Storage storage = getStorageInstance(); + assert storage != null; + assertThrows(StorageException.class, () -> { + storage.deleteFile(INVALID_FILENAME); + }, EXPECTED_EXCEPTION_FILENAME); + } + +} diff --git a/src/test/java/wellnus/ui/TextUiTest.java b/src/test/java/wellnus/ui/TextUiTest.java new file mode 100644 index 0000000000..c04e4404b8 --- /dev/null +++ b/src/test/java/wellnus/ui/TextUiTest.java @@ -0,0 +1,129 @@ +package wellnus.ui; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Scanner; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test the functionality of methods associated with TextUi class. + */ +class TextUiTest { + private static final String DEFAULT_SEPARATOR = "----------------------------------------" + + "--------------------"; + private static final String ALERT_SEPARATOR = "!!!!!!-------!!!!!--------!!!!!!!------!!!!!" + + "---------!!!!!!!"; + private static final String TEST_OUTPUT_MSG_ONE = "Hello "; + private static final String TEST_OUTPUT_MSG_TWO = "World"; + private static final String OUTPUT_MSG_ONE = "Hello"; + private static final String INDENTATION = " "; + private static final String ARITHMETIC_EXCEPTION_MSG_ONE = "Please check your arithmetic equation!!"; + private static final String ARITHMETIC_EXCEPTION_MSG_TWO = "E.g. Denominator is 0 in division."; + private static final String ERROR_MESSAGE_LABEL = "Error Message:"; + private static final String EXTRA_MESSAGE_LABEL = "Note:"; + private static final String GREET_MSG = "How are you?"; + private static final String OPERATION = "/ by zero"; + private static final String INPUT_WHITESPACE = " My string "; + private static final String INPUT_WITHOUT_WHITESPACE = "My string"; + private static final TextUi UI = new TextUi(); + private static final int TEST_NUMERATOR = 2; + private static final int TEST_DENOMINATOR = 0; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + + /** + * Read test input command and return back the command string.
    + * For JUnit testing purpose only. + * + * @param readInput Scanner object with System.in being overwritten in test + * @return User input command with leading/dangling whitespace being removed + */ + public static String getCommand(Scanner readInput) { + String inputLine = readInput.nextLine(); + String userCommand = inputLine.trim(); + return userCommand; + } + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + /** + * Test whether removal of command leading/dangling space is successful.
    + * Expect success. + */ + @Test + void getCommand_trimSpace_success() { + InputStream sysInBackup = System.in; + ByteArrayInputStream in = new ByteArrayInputStream((INPUT_WHITESPACE + + System.lineSeparator()).getBytes()); + System.setIn(in); + Scanner readLine = new Scanner(System.in); + String command = getCommand(readLine); + assertEquals(INPUT_WITHOUT_WHITESPACE, command); + System.setIn(sysInBackup); + readLine.close(); + } + + /** + * Test whether default line separator is properly drawn.
    + * Expect success. + */ + @Test + void printSeparator_defaultVersion_success() { + UI.printSeparator(); + assertEquals(DEFAULT_SEPARATOR, outputStreamCaptor.toString().trim()); + } + + /** + * Test whether exception message will be printed properly with correct format.
    + * Expect both error message and error notes. + */ + @Test + void printErrorFor_arithmeticException_success() { + String errorMsg = ARITHMETIC_EXCEPTION_MSG_ONE + System.lineSeparator() + ARITHMETIC_EXCEPTION_MSG_TWO; + try { + int result = TEST_NUMERATOR / TEST_DENOMINATOR; + } catch (ArithmeticException exception) { + UI.printErrorFor(exception, errorMsg); + } + assertEquals(ALERT_SEPARATOR + System.lineSeparator() + ERROR_MESSAGE_LABEL + + System.lineSeparator() + INDENTATION + OPERATION + System.lineSeparator() + + EXTRA_MESSAGE_LABEL + System.lineSeparator() + INDENTATION + ARITHMETIC_EXCEPTION_MSG_ONE + + System.lineSeparator() + INDENTATION + ARITHMETIC_EXCEPTION_MSG_TWO + System.lineSeparator() + + ALERT_SEPARATOR, + outputStreamCaptor.toString().trim()); + } + + /** + * Test whether messages will be properly printed with correct format. + */ + @Test + void printOutputMessage_greeting_success() { + UI.printOutputMessage(GREET_MSG); + assertEquals(DEFAULT_SEPARATOR + System.lineSeparator() + + INDENTATION + GREET_MSG + System.lineSeparator() + DEFAULT_SEPARATOR, + outputStreamCaptor.toString().trim()); + } + + /** + * Test whether multi-line message can be printed with correct indentation.
    + * Expect string split by lineSeparator() and indentation in front of each new line. + */ + @Test + void printMultiLineMessage_twoLines_success() { + String multiLineOutput = TEST_OUTPUT_MSG_ONE + System.lineSeparator() + TEST_OUTPUT_MSG_TWO; + UI.printOutputMessage(multiLineOutput); + assertEquals((DEFAULT_SEPARATOR + System.lineSeparator() + INDENTATION + OUTPUT_MSG_ONE + + System.lineSeparator() + INDENTATION + TEST_OUTPUT_MSG_TWO + + System.lineSeparator() + DEFAULT_SEPARATOR), + outputStreamCaptor.toString().trim()); + } +} + diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..39c2aed1b5 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,27 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +------------------------------------------------------------ + Very good day to you! Welcome to + + ,--. ,--. ,--.,--.,--. ,--.,--. ,--. ,---. | | | | + | | | | ,---. | || || ,'.| || | | |' .-',---| |---.,---| |---. + | |.'.| || .-. :| || || |' ' || | | |`. `-.'---| |---''---| |---' + | ,'. |\ --.| || || | ` |' '-' '.-' | | | | | + '--' '--' `----'`--'`--'`--' `--' `-----' `-----' `--' `--' +------------------------------------------------------------ +------------------------------------------------------------ + Enter a command to start using WellNUS++! Try 'help' if you're new, or just unsure. +------------------------------------------------------------ +(main):~$ !!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! +Error Message: + Invalid command issued! +Note: + Supported features: + Access Atomic Habit: hb + Access Self Reflection: reflect + Access Focus Timer: ft + Access Gamification: gamif + Help command: help + Exit program: exit +!!!!!!-------!!!!!--------!!!!!!!------!!!!!---------!!!!!!! +(main):~$ ------------------------------------------------------------ + Thank you for using WellNUS++! See you again soon Dx +------------------------------------------------------------ diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..00b7c8c0da 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,2 @@ -James Gosling \ No newline at end of file +test +exit \ No newline at end of file