diff --git a/.project b/.project index 1c9339c5f927..f7b9cea3452b 100644 --- a/.project +++ b/.project @@ -1,7 +1,7 @@ - addressbook-level4 - Project addressbook-level4 created by Buildship. + Agendum + Agendum created by Buildship. diff --git a/.travis.yml b/.travis.yml index a9d9e9b47d87..fd94446e103b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,3 +11,8 @@ addons: apt: packages: - oracle-java8-installer + +notifications: + email: + on_success: change + on_failure: always diff --git a/README.md b/README.md index 249a00b3899c..7bf6209f5418 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,47 @@ -[![Build Status](https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master)](https://travis-ci.org/se-edu/addressbook-level4) -[![Coverage Status](https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master)](https://coveralls.io/github/se-edu/addressbook-level4?branch=master) +[![Build Status](https://travis-ci.org/CS2103AUG2016-W11-C2/main.svg?branch=master)](https://travis-ci.org/CS2103AUG2016-W11-C2/main) +[![Coverage Status](https://coveralls.io/repos/github/CS2103AUG2016-W11-C2/main/badge.svg?branch=master)](https://coveralls.io/github/CS2103AUG2016-W11-C2/main?branch=master) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/b2c2cb7e938f4d9eb626926dc3670f3c)](https://www.codacy.com/app/vishnu/main?utm_source=github.com&utm_medium=referral&utm_content=CS2103AUG2016-W11-C2/main&utm_campaign=Badge_Grade) -# Address Book (Level 4) +# Agendum -
+
-* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using - a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as - the main programming language. -* It is **written in OOP fashion**. It provides a **reasonably well-written** code example that is - **significantly bigger** (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from [level 3](https://github.com/se-edu/addressbook-level3): - * A more sophisticated GUI that includes a list panel and an in-built Browser. - * More test cases, including automated GUI testing. - * Support for *Build Automation* using Gradle and for *Continuous Integration* using Travis CI. +### Table of Contents +* [Introduction](#introduction) +* [Site Map](#site-map) +* [Acknowledgements](#acknowledgements) +* [License](#license) + + +### Introduction +Hello! Do you have too many tasks and are unable to finish all of them? Are you looking for a hassle-free task manager which works swiftly? +Enter Agendum. + +This task manager will assist you in completing all your tasks on time. It will automatically sort your tasks by date, so you can always see the most urgent tasks at the top of list! + +With just one line of command, Agendum will carry out your wishes. You don’t ever have to worry about having to click multiple links. It is even capable of allowing you to create your own custom commands! This means that you can get things done even faster, your way. -#### Site Map +### Site Map * [User Guide](docs/UserGuide.md) * [Developer Guide](docs/DeveloperGuide.md) -* [Learning Outcomes](docs/LearningOutcomes.md) * [About Us](docs/AboutUs.md) * [Contact Us](docs/ContactUs.md) -#### Acknowledgements - -* Some parts of this sample application were inspired by the excellent - [Java FX tutorial](http://code.makery.ch/library/javafx-8-tutorial/) by *Marco Jakob*. +### Acknowledgements +Our team would like to thank the following people: +* Professor [Damith C. Rajapakse](http://www.comp.nus.edu.sg/~damithch) + for giving us invaluable advice on Software Engineering +* TA [Muthu Kumar Chandrasekaran](https://www.quora.com/profile/Muthu-Kumar-Chandrasekaran) + who has given us constructive feedback to aid in our development of Agendum +* The development team of Address Book (Level 4) which can be found at this + [GitHub repo](https://github.com/nus-cs2103-AY1617S1/addressbook-level4) +* [Joe Stelmach](http://natty.joestelmach.com/) who created the Natty Date Parser which powers our date-time parsing. +* Marco Jakob for providing [Java FX tutorial](http://code.makery.ch/library/javafx-8-tutorial/) +* [Google Calendar API](https://developers.google.com/google-apps/calendar/) +* [Mockito](http://site.mockito.org/) -#### Licence : [MIT](LICENSE) +### License +* [MIT](LICENSE) diff --git a/build.gradle b/build.gradle index 46b06c1e42ec..2a20067691e0 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,13 @@ allprojects { junitVersion = '4.12' testFxVersion = '4.0.+' monocleVersion = '1.8.0_20' + nattyVersion = '0.12' + reflectionsVersion = '0.9.10' + googleCalendarVersion = 'v3-rev220-1.22.0' + googleHttpVersion = '1.21.0' + googleOauthVersion = '1.21.0' + mockitoVersion = '1.+' + libDir = 'lib' } @@ -52,6 +59,12 @@ allprojects { compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonDataTypeVersion" compile "com.google.guava:guava:$guavaVersion" + compile "com.joestelmach:natty:$nattyVersion" + compile "org.reflections:reflections:$reflectionsVersion" + compile "com.google.apis:google-api-services-calendar:$googleCalendarVersion" + compile "com.google.http-client:google-http-client-jackson2:$googleHttpVersion" + compile "com.google.oauth-client:google-oauth-client-jetty:$googleOauthVersion" + compile "org.mockito:mockito-core:$mockitoVersion" testCompile "junit:junit:$junitVersion" testCompile "org.testfx:testfx-core:$testFxVersion" @@ -74,10 +87,10 @@ allprojects { } shadowJar { - archiveName = "addressbook.jar" + archiveName = "agendum.jar" manifest { - attributes "Main-Class": "seedu.address.MainApp" + attributes "Main-Class": "seedu.agendum.MainApp" } destinationDir = file("${buildDir}/jar/") @@ -113,8 +126,8 @@ tasks.coveralls { onlyIf { System.env.'CI' } } -class AddressBookTest extends Test { - public AddressBookTest() { +class AgendumTest extends Test { + public AgendumTest() { forkEvery = 1 systemProperty 'testfx.setup.timeout', '60000' } @@ -128,7 +141,7 @@ class AddressBookTest extends Test { } } -task guiTests(type: AddressBookTest) { +task guiTests(type: AgendumTest) { include 'guitests/**' jacoco { @@ -137,8 +150,8 @@ task guiTests(type: AddressBookTest) { } -task nonGuiTests(type: AddressBookTest) { - include 'seedu/address/**' +task nonGuiTests(type: AgendumTest) { + include 'seedu/agendum/**' jacoco { destinationFile = new File("${buildDir}/jacoco/test.exec") @@ -146,10 +159,15 @@ task nonGuiTests(type: AddressBookTest) { } // Test mode depends on whether headless task has been run -task allTests(type: AddressBookTest) { +task allTests(type: AgendumTest) { jacoco { destinationFile = new File("${buildDir}/jacoco/test.exec") } + + exec { + executable 'rm' + args '-vf', 'data/StoredCredential' + } } task headless << { diff --git a/cal/StoredCredential_1 b/cal/StoredCredential_1 new file mode 100644 index 000000000000..cef2ff6b3bee Binary files /dev/null and b/cal/StoredCredential_1 differ diff --git a/cal/StoredCredential_2 b/cal/StoredCredential_2 new file mode 100644 index 000000000000..a6dc72de1932 Binary files /dev/null and b/cal/StoredCredential_2 differ diff --git a/cal/StoredCredential_3 b/cal/StoredCredential_3 new file mode 100644 index 000000000000..c1147bbd0d9c Binary files /dev/null and b/cal/StoredCredential_3 differ diff --git a/collated/docs/A0003878Y.md b/collated/docs/A0003878Y.md new file mode 100644 index 000000000000..7af7a6d68a00 --- /dev/null +++ b/collated/docs/A0003878Y.md @@ -0,0 +1,177 @@ +# A0003878Y +###### /DeveloperGuide.md +``` md +### 3. Logic component + +`Logic` provides several APIs for `UI` to execute the commands entered by the user. It also obtains information about the to-do list to render to the user. +The **API** of the logic component can be found at [`Logic.java`](../src/main/java/seedu/agendum/logic/Logic.java) + +The class diagram of the Logic Component is given below. `LogicManager` implements the `Logic Interface` and has exactly one `Parser`. `Parser` is responsible for processing the user command and creating instances of concrete `Command` objects (such as `AddCommand`) which will then be executed by the `LogicManager`. New command types must implement the `Command` class. Each `Command` class produces exactly one `CommandResult`. + +
+ +The `CommandLibrary` class is responsible for managing the various Agendum's reserved command keywords and their aliases. The `Parser` checks and queries the `CommandLibrary` to ascertain if a command word given has been aliased to a reserved command word. `AliasCommand` and `UnaliasCommand` will also check and update the `CommandLibrary` to add and remove aliases. The singleton pattern is applied to restrict the instantiation of the class to one object. This is to ensure that all other objects, such as `Parser`, `AliasCommand` and `UnaliasCommand` objects will refer to the same instance that records and manages all the alias relationships. + +You can view the Sequence Diagram below for interactions within the `Logic` component for the `execute("delete 1")` API call.
+ +
+ +#### Command Pattern +The Parser creates concrete Command objects such as `AddCommand` objects. `LogicManager` will then execute the various commands, such as `AddCommand` and `UndoCommand`. Each command does a different task and gives a different result. However, as all command types inherit from the abstract class `Command` and implement the _`execute`_ method, LogicManager (the invoker) can treat all of them as Command Object without knowing each specific Command type. By calling the _`execute`_ method, different actions result. + + +``` +###### /DeveloperGuide.md +``` md +## Appendix C : Non Functional Requirements + +1. Should work on any [mainstream OS](#mainstream-os) as long as it has Java `1.8.0_60` or higher installed. +2. Should be able to hold up to 800 tasks in total (including completed tasks). +3. Should come with automated unit tests. +4. Should use a Continuous Integration server for real time status of master’s health. +5. Should be kept open source code. +6. Should favour DOS style commands over Unix-style commands. +7. Should adopt an object oriented design. +8. Should not violate any copyrights. +9. Should have a response time of less than 2 second for every action performed. +10. Should work offline without an internet connection. +11. Should work as a standalone application. +12. Should not use relational databases to store data. +13. Should store data in an editable text file. +14. Should not require an installer. +15. Should not use paid libraries and frameworks. +16. Should be a free software. +17. Should be easily transferrable between devices; only 1 folder needs to be transferred. +18. Should have documentation that matches the source code +19. Should not have unhandled exceptions from user input +20. Should be installable without assistance other than the user guide. +21. Should have understandable code such that new members can start working on the project within 1 week. + + +  + + +``` +###### /UserGuide.md +``` md +## Getting Started + +### Download + +1. Ensure that you have Java version `1.8.0_60` or above installed on your computer. +2. Download the latest `Agendum.jar` from [here](../../../releases).
+
+3. Copy the jar file to the folder that you intend to use as the root directory of Agendum. + +### Launch + +To launch Agendum, double-click on `Agendum.jar` to launch Agendum. Welcome! + +Here is the main window you will be greeted with. Initially the task panels are empty but fill them up with tasks soon. + +
+ +``` +###### /UserGuide.md +``` md +## Features + +### Commands + +**Here are some general things to note:** +> * All command words are case-insensitive. e.g. `Add` will match `add` +> * Words enclosed in angle brackets, e.g.`` are the parameters. You can freely decide what you want to use in its place. +> * Parameters with `...` after them can have multiple instances (separated by whitespace). For example, `...` means that you can specify multiple indices such as `3 5 7`. + + +### Adding a task: `add` + +If you have a task to work on, add it to the Agendum to start keeping track!
+ +Here are the *acceptable format(s)*: + +* `add ` - adds a task which can be done anytime. +* `add by ` - adds a task which have to be done by the specified deadline. Note the keyword `by`. +* `add from to ` - adds a event which will take place between start time and end time. Note the keyword `from` and `to`. + +Here are some *examples*: + +``` +Description: I want to watch Star Wars but I don't have a preferred time. +> add watch Star Wars +Result: Agendum will add a task to the "Do It Anytime" panel. + +Description: I need to return my library books by the end of this week. +> add return library books by Friday 8pm +Result: Agendum will add a task "return library books" to the "Do It Soon" panel. +It will have a deadline set to the nearest upcoming Friday and with time 8pm. + +Description: I have a wedding dinner which will take place on 30 Oct night. +> add attend wedding dinner from 30 Oct 7pm to 30 Oct 9.30pm +Result: Agendum will add a task "attend wedding dinner" to the "Do It Soon" panel. +It will have a start time 30 Oct 7pm and end time 30 Oct 9.30pm. +``` + +> A task cannot have both a deadline and a event time. + +Did Agendum intepret part of your task name as a deadline/event time when you did not intend for it to do so? Simply `undo` the last command and remember to enclose your task name with **single** quotation mark this time around. +``` +> add 'drop by 7 eleven' by tmr +``` + +#### Date Time Format +How do you specify the ``, `` and `` of a task? + +Agendum supports a wide variety of date time formats. Combine any of the date format and time format below. The date/time formats are case insensitive too. + +*Date Format* + +| Date Format | Example(s) | +|-----------------|----------------------| +| Month/day | 1/23 | +| Day Month | 1 Oct | +| Month Day | Oct 1 | +| Day of the week | Wed, Wednesday | +| Relative date | today, tmr, next wed | + + > If no year is specified, it is always assumed to be the current year. + > It is possible to specify the year before or after the month-day pair in the first 3 formats (e.g. 1/23/2016 or 2016 1 Oct) + > The day of the week refers to the following week. For example, today is Sunday (30 Oct). Agendum will interpret Wednesday and Sunday as 2 Nov and 6 Nov respectively (a week from now). + +*Time Format* + +| Time Format | Example(s) | +|-----------------|-----------------------------------------| +| Hour | 10, 22 | +| Hour:Minute | 10:30 | +| Hour.Minute | 10.30 | +| Relative time | this morning, this afternoon, tonight | + +> By default, we use the 24 hour time format but we do support the meridian format as well e.g. 10am, 10pm + +Here are some examples of the results if these formats are used in conjunction with the `add` command. +``` +> add submit homework by 9pm +Result: The day is not specified. Agendum will create a task "submit homework" +with the deadline day as today (the date of creation) and time as 9pm + +> add use coupons by next Wed +Result: The time is not specified. Agendum will create a task "use coupons" +with deadline day as the upcoming Wednesday and time as the current time. + +> add attend wedding dinner from 10 Nov 8pm to 10 Nov 9pm +Result: All the date and time are specified and there is no ambiguity at all. +``` + +Note +> If no year, date or time is specified, the current year, date or time will be used. +> It is advisable to specify both the date and time. + + +Helpful tip: With Agendum, you can skip typing the second date only if the deadline/event is **happening some time in the future** +``` +> add attend wedding dinner from 30 Nov 8pm to 9pm +``` + + +``` diff --git a/collated/docs/A0133367E.md b/collated/docs/A0133367E.md new file mode 100644 index 000000000000..df12957da633 --- /dev/null +++ b/collated/docs/A0133367E.md @@ -0,0 +1,506 @@ +# A0133367E +###### /DeveloperGuide.md +``` md +### 1. Architecture + +
+ +The **_Architecture Diagram_** given above summarizes the high-level design of Agendum. +Here is a quick overview of the main components of Agendum and their main responsibilities. + +#### `Main` +The **`Main`** component has a single class: [`MainApp`](../src/main/java/seedu/agendum/MainApp.java). It is responsible for initializing all components in the correct sequence and connecting them up with each other at app launch. It is also responsible for shutting down the other components and invoking the necessary clean up methods when Agendum is shut down. + + +#### `Commons` +[**`Commons`**](#6-common-classes) represents a collection of classes used by multiple other components. +Two Commons classes play important roles at the architecture level. + +* `EventsCentre` (written using [Google's Event Bus library](https://github.com/google/guava/wiki/EventBusExplained)) + is used by components to communicate with other components using events. +* `LogsCenter` is used by many classes to write log messages to Agendum's log file to record noteworthy system information and events. + + +#### `UI` +The [**`UI`**](#2-ui-component) component is responsible for interacting with the user by accepting commands, displaying data and results such as updates to the task list. + + +#### `Logic` +The [**`Logic`**](#3-logic-component) component is responsible for processing and executing the user's commands. + + +#### `Model` +The [**`Model`**](#4-model-component) component is responsible for representing and holding Agendum's data. + + +#### `Storage` +The [**`Storage`**](#5-storage-component) component is responsible for reading data from and writing data to the hard disk. + + +Each of the `UI`, `Logic`, `Model` and `Storage` components: + +* Defines its _API_ in an `interface` with the same name as the Component +* Exposes its functionality using a `{Component Name}Manager` class. + +For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` +interface and exposes its functionality using the `LogicManager.java` class.
+
+ + +#### Event Driven Approach +Agendum applies an Event-Driven approach and the **Observer Pattern** to reduce direct coupling between the various components. For example, the `UI` and `Storage` components are interested in receiving notifications when there is a change in the to-do list in `Model`. To avoid bidrectional coupling, `Model` does not inform these components of changes directly. Instead, it posts an event and rely on the `EventsCenter` to notifying the register Observers in `Storage` and `UI`. + +Consider the scenario where the user inputs `delete 1` described in the _Sequence Diagram_ below. The `UI` component will invoke the `Logic` component’s _execute_ method to carry out the given command, `delete 1`. The `Logic` component will identify the corresponding task and will call the `Model` component _deleteTasks_ method to update Agendum’s data and raise a `ToDoListChangedEvent`. + + + +The diagram below shows what happens after a `ToDoListChangedEvent` is raised. `EventsCenter` will inform its subscribers. `Storage` will respond and save the changes to hard disk while `UI` will respond and update the status bar to reflect the 'Last Updated' time.
+ + + +#### Model-View-Controller approach +To further reduce coupling between components, the Model-View-Controller pattern is also applied. The 3 components are as follows: +* Model: The `Model` component as previously described, maintains and holds Agendum's data. +* View: Part of the `UI` components and resources such as the .fxml file is responsible for displaying Agendum's data and interacting with the user. Through events, the `UI` component is able to get data updates from the model. +* Controller: Parts of the `UI` component such as (`CommandBox`) act as 'Controllers' for part of the UI. The `CommandBox` accepts user command input and request `Logic` to execute the command entered. This execution may result in changes in the model. + + +#### Activity Diagram + +
+ +The Activity Diagram above illustrates Agendum's workflow. Brown boxes represent actions taken by Agendum while orange boxes represent actions that involve interaction with the user. + +After Agendum is launched, Agendum will wait for the user to enter a command. Every command is parsed. If the command is valid and adheres to the given format, Agendum will executes the command. Agendum `Logic` component checks the input such as indices before updating the model and storage if needed. + +Agendum will then display changes in the to-do list and feedback of each command in the UI. The user can then enter a command again. Agendum will also give pop-up feedbacks when the command format or inputs are invalid. + +The following sections will then give more details of each individual component. + + +``` +###### /DeveloperGuide.md +``` md +### 4. Model component + +As mentioned above, the `Model` component stores and manages Agendum's task list data and user's preferences. It also exposes a `UnmodifiableObservableList` that can be 'observed' by other components e.g. the `UI` can be bound to this list and will automatically update when the data in the list change. + +Due to the application of the **Observer Pattern**, it does not depend on other components such as `Storage` but interact by raising events instead. + +The `Model` class is the interface of the `Model` component. It provides several APIs for the `Logic` and `UI` components to update and retrieve Agendum’s task list data. The **API** of the model component can be found at [`Model.java`](../src/main/java/seedu/agendum/model/Model.java). + +The structure and relationship of the various classes in the `Model` component is described in the diagram below. + +
+ +`ModelManager` implements the `Model` Interface. It contains a `UserPref` Object which represents the user’s preference and a `SyncManager` object which is necessary for the integration with Google calendar. + +`SyncManager` implements the `Sync` Interface. The `SyncManager` redirects the job of syncing a task to a `SyncProvider`. In Agendum, we have one provider, `SyncProviderGoogle` that implements the `SyncProvider` Interface. This is done so that it would be easy to extend Agendum to sync with other providers. One would just have to create a new class that extends the `SyncProvider` Interface and register that class with `SyncManager`. + +`ModelManager` contains a **main** `ToDoList` object and a stack of `ToDoList` objects referred to as `previousLists`. The **main** `ToDoList` object is the copy that is indirectly referred to by the `UI` and `Storage`. The stack, `previousLists` is used to support the [`undo` operation](#### undo). + +Each `ToDoList` object has one `UniqueTaskList` object. A `UniqueTaskList` can contain multiple `Task` objects but does not allow duplicates. + +The `ReadOnlyToDoList` and `ReadOnlyTask` interfaces allow other classes and components, such as the `UI`, to access but not modify the list of tasks and their details. + +Currently, each `Task` has a compulsory `Name` and last updated time. It is optional for a `Task` to have a start and end time. Each `Task` also has a completion status which is represented by a boolean. + +Design considerations: +> * `ToDoList` is a distinct class from `UniqueTaskList` as it can potentially be extended to have another `UniqueTagList` object to keep track of tags associated with each task and `ToDoList` will be responsible for syncing the tasks and tags. +> * `Name` is a separate class as it might be modified to have its own validation regex e.g. no / or " + +Using the same example, if the `Logic` component requests `Model` to _deleteTasks(task)_, the subsequent interactions between objects can be described by the following sequence diagram. + + + +The identified task is removed from the `UniqueTaskList`. The `ModelManager` raises a `ToDoListChangedEvent` and back up the current to do list to `previousLists` + +> `Model`’s _deleteTasks_ methods actually take in `ArrayList` instead of a single task. We use _deleteTasks(task)_ for simplicity in the sequence diagram. + + +#### undo + +`previousLists` is a Stack of `ToDoList` objects with a minimum size of 1. The `ToDoList` object at the top of the stack is identical to the **main** `ToDoList` object before any operation that mutate the to-do list is performed and after any operation that mutates the task list successfully (i.e. without exceptions). + +This is achieved with the _backupCurrentToDoList_ function which pushes a copy of the **main** `ToDoList` to the stack after any successful changes, such as the marking of multiple tasks. + +To undo the most recent changes, we simply pop the irrelevant `ToDoList` at the top of the `previousLists` stack and copy the `ToDoList` at the top of the stack back to the **main** list + +This approach is reliable as it eliminates the need to implement an "undo" method and store the changes separately for each command that will mutate the task list. + +Also, it helps to resolve the complications involved with manipulating multiple task objects at a go. For example, the user might try to mark multiple tasks and one of which will result in a `DuplicateTaskException`. To revert the undesired changes to the **main** `ToDoList`, we can copy the the `ToDoList` at the top of the stack back to the **main** list. In such unsuccessful operations, the changes would not have persisted to Storage. + + +``` +###### /DeveloperGuide.md +``` md +## Implementation + +### 1. Logging + +We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels +and logging destinations. + +* The logging level can be controlled using the `logLevel` setting in the configuration file + (See [Configuration](#2-configuration)) +* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to + the specified logging level +* Currently log messages are output through `Console` and to a `.log` file. + +**Logging Levels** + +Currently, Agendum has 4 logging levels: `SEVERE`, `WARNING`, `INFO` and `FINE`. They record information pertaining to: + +* `SEVERE` : A critical problem which may cause the termination of Agendum
+ e.g. fatal error during the initialization of Agendum's main window +* `WARNING` : A problem which requires attention and caution but allows Agendum to continue working
+ e.g. error reading from/saving to config file +* `INFO` : Noteworthy actions by Agendum
+ e.g. valid and invalid commands executed and their results +* `FINE` : Less significant details that may be useful in debugging
+ e.g. all fine details of the tasks including their last updated time + +### 2. Configuration + +You can alter certain properties of our Agendum application (e.g. logging level) through the configuration file. +(default:`config.json`). + + +  + + +``` +###### /DeveloperGuide.md +``` md +### Use case 05 - Undo previous command that modified the task list + +**MSS** + +1. Actor requests to undo the last change to the task list. +2. System revert the last change to the task list. +3. System shows a success feedback message and displays the updated list. + Use case ends. + +**Extensions** + +2a. There are no previous modifications to the task list (since the launch of the application) + +> 2a1. System alerts the user that there are no previous changes
+> Use case ends + + +### Use case 06 - Mark a task as completed + +**MSS**: + +1. Actor requests to mark a task specified by its index in the list as completed +2. System marks the task as completed +3. System shows a success feedback message, updates and highlights the selected task. + Use case ends + +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends + +2a. Marking a task will result in a duplicate (will become exactly identical to an existing task) + +> 2a1. System shows an error message to inform user of potential duplicate
+> Use case ends + +### Use case 07 - Unmark a task + +**MSS**: + +1. Actor requests to unmark a task followed by its index +2. System unmarks the task from completed +3. System shows a success feedback message, updates and highlights the selected task. + Use case ends + +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends + +2a. Unmarking a task will result in a duplicate (will become exactly identical to an existing task) + +> 2a1. System shows an error message to inform user of potential duplicate
+> Use case ends + + +``` +###### /DeveloperGuide.md +``` md +## Appendix E : Product Survey + +We conducted a product survey on other task managers. Here is a summary of the strengths and weaknesses of each application. The criteria used for evaluation are own preferences and Jim's requirements. + +#### Main insights +* Keyboard friendliness of our application is extremely important. It is useful to distinguish our application from the rest. Keyboard shortcuts must be intuitive, easy to learn and remember. + * Tab for autocomplete + * Scroll through command history or task list with up and down + * Allow users to specify their own shorthand commands so they will remember + * Summoning the help window with a keyboard shortcut +* Clear visual feedback on the status of the task + * Overdue and upcoming tasks should stand out + * Should also be able to see if a task is completed or recurring + * Identify if the task is selected/has just been updated +* Organized overview of all tasks + * Tasks should be sorted by their deadline/happening time + * Users might like to see their recently updated/completed tasks at the top of the list + * Allow user to see these various types of tasks and distinguish them without having to switch between lists (i.e. have multiple lists) +* Will be nice to allow more details for tasks + * detailed task descriptions + * tagging +* Commands should be intuitive and simple enough for new users + * more natural language like parsing for dates with prepositions as keywords + + +#### Wunderlist + +*Strengths:* + +* Clearly displays tasks that have not been completed +* Tasks can be categorized under different lists +* Tasks can have sub tasks +* Possible to highlight tasks by marking as important (starred) or pinning tasks +* Can set deadlines for tasks +* Can create recurring tasks +* Can associate files with tasks +* Can be used offline +* Keyboard friendly – keyboard shortcuts to mark tasks as completed and important +* Search and sort functionality makes finding and organizing tasks easier +* Possible to synchronize across devices +* Give notifications and reminders for tasks near deadline or overdue + +*Weaknesses:* + +* Wunderlist has a complex interface and might require multiple clicks to get specific tasks done. For example, it has separate field to add tasks, search for tasks and a sort button. There are various lists & sub-lists. Each list has a completed/uncompleted section and each task needs to be clicked to display the associated subtasks, notes, files and comment. +* New users might not know how to use the advanced features e.g. creating recurring tasks + +#### Google calendar + +*Strengths:* + +* Have a weekly/monthly/daily calendar view which will make it easy for users to visualize their schedules +* Can create recurring events +* Integrated with Gmail. A user can add events from emails easily and this is desirable since Jim's to do items arrive by emails +* Can be used offline +* Possible to synchronize across devices +* Calendar can be exported to CSV/iCal for other users +* CLI to quick add an event to a calendar instead of clicking through the screen +* Comprehensive search by name/details/people involved/location/time + + +*Weaknesses:* + +* Not possible to mark tasks as completed +* Not possible to add tasks without deadline or time +* CLI does not support updating of tasks/deleting etc. Still requires clicking. +* New users might not know of the keyboard shortcuts +``` +###### /UserGuide.md +``` md +### Visual Introduction + +Here is what Agendum may look like with some tasks added and completed. + +
+ +Notice how Agendum has 3 panels: **"Do It Soon"**, **"Do It Anytime"** and **"Done"**. +* **"Do It Soon"** panel will show your **uncompleted** tasks with deadlines and events. Those tasks demand your attention at or by some specific time! Agendum has helpfully sorted these tasks by their deadline or event time. + * **Overdue** tasks _(e.g. tutorial)_ will stand out in red at the top of the list. + * **Upcoming** tasks (happening/due within a week) _(e.g. essay draft)_ will stand out in light green next. +* **"Do It Anytime"** panel will show your **uncompleted** tasks which you did not specify a deadline or happening time. Do these tasks anytime. +* **"Done"** panel will show all your completed tasks. To make it easier for you to keep track of what you have done recently, Agendum will always show the latest completed tasks at the top of the list. + +Agendum will clearly display the name and time associated with each task. Notice that each task is displayed with a ID. For example, the task *learn piano* has a ID *7* now. We will use this ID to refer to the task for some Agendum commands. + +The **Command Box** is located at the top of Agendum. Enter your keyboard commands into the box! +Just in case, there is a **Status Bar** located at the bottom of Agendum. You can check today's date and time, where your Agendum's to-do list data is located and when your data was last saved. + +Agendum also has a pretty **Help Window** which summarizes the commands you can use. Agendum might show pop-ups and highlights after each commands for you to review your changes. + +``` +###### /UserGuide.md +``` md +### Renaming a task : `rename` + +Agendum understands that plans and tasks change all the time.
+ +If you wish to update the description of a task, you can use the following *format*: + +* `rename ` - give a new name to the task identified by ``. The `` must be a positive number and be in the most recent to-do list displayed. + +Here is an *example*:
+
+ +``` +Description: I want to be more specific about the movie I want to watch for task id #2. +To update the name of the task, +> rename 2 watch Harry Potter +``` + +Agendum will promptly update the displayed task list!
+
+ + +### (Re)scheduling a task : `schedule` + +Agendum recognizes that your schedule might change, and therefore allows you to reschedule your tasks easily. + +Here are the *acceptable format(s)*: + +* `schedule ` - re-schedule the task identified by ``. It can now be done anytime. It is no longer bounded by a deadline or event time! +* `schedule by ` - set or update the deadline for the task identified. Note the keyword `by`. +* `schedule from to ` - update the start/end time of the task identified by ``. Note the keyword `from` and `to`. + +Note: + > * Again, `` must be a positive number and be in the most recent to-do list displayed. + > * ``, `` and `` must follow the format previously defined in [Date Time Format](#date-time-format) + > * A task cannot have both a deadline and a event time. + +Here are some *examples*:
+
+ +``` +Description: I decide that I can go for a run at any time instead. +> schedule 1 +Result: Agendum will start/end time of the task "go for a run" and it will +move to the "Do It Anytime" panel + +Description: I want to submit my reflection earlier. +> schedule 2 by tmr 2pm +Result: Agendum will update the deadline of "submit personal reflection". It +will then be sorted in the "Do It Soon" panel. +``` + +Agendum will promptly update the displayed task list!
+
+ + +### Marking a task as completed : `mark` + +Have you completed a task? Well done!
+Celebrate the achievement by recording this in Agendum. + +Here is the *format*: +* `mark ...` - mark all the tasks identified by ``(s) as completed. Each `` must be a positive number and in the most recent to-do list displayed. + +``` +Description: I just walked my dog! +> mark 4 +Result: Agendum will move "walk the dog" to the "Done" panel + +Description: I had a really productive day and did all the other tasks too. +> mark 1 2 3 +Result: Agendum will save you the hassle of marking each individual task as +completed one by one. It is satisfying to watch how all the tasks move to the +"Done" panel together. + +You can also try out any of the following examples: +> mark 1,2,3 +> mark 1-3 +The tasks with display ids 1, 2 and 3 will be marked as completed. +``` + +* You can specify a id (e.g. 1) or a range of id (e.g. 3-8). They must be separated by whitespace (e.g. 1 2 3) or commas (e.g. 2,3) + +The changes are as shown below.
+
+ + +### Unmarking a task : `unmark` + +You might change your mind and want to continue working on a recently completed task. +They will conveniently be located at the top of the done panel. + +To reflect the change in completion status in Agendum, here is the *format*: +* `unmark ...` - unmark all the tasks identified by ``s as completed. Each `` must be a positive number and in the most recent to-do list displayed. + +This works in the same way as the `mark` command. The tasks will then be moved to the **"Do It Soon"** or **"Do It Anytime"** panel accordingly.
+ + +### Deleting a task : `delete` + +We understand that there are some tasks which will never get done and are perhaps no longer relevant.
+You can remove these tasks from the task list to keep these tasks out of sight and out of mind. + +Here is the *format*: +* `delete ...` - delete all the tasks identified by ``s as completed. Each `` must be a positive number and in the most recent to-do list displayed. + +Here are some *examples*:
+
+ +``` +Description: I just walked my dog and no longer want to view this task anymore. +> delete 4 +Result: Agendum will delete the task "walk the dog" and it will no longer +appear in any of the 3 panels. + +Description: I do not want to view the tasks at all. +> delete 1 2 3 +Result: Agendum will save you the hassle of deleting each individual task but +still allows you to selectively choose what to delete. +You can also try out any of the following examples: +> delete 1,2,3 +> delete 1-3 +The tasks with display ids 1, 2 and 3 will be deleted. +``` + +* You can specify a id (e.g. 1) or a range of id (e.g. 3-8). They must be separated by whitespace (e.g. 1 2 3) or commas (e.g. 2,3) + +The deleted tasks will appear in a popup window.
+
+ + + +### Undoing your last changes : `undo` + +Agendum understands that you might make mistakes and change your mind. Hence, Agendum does offer some flexibility and allow you to reverse the effects of a few commands by simply typing `undo`. Multiple and successive `undo` are supported. + +Commands that can be "undone" include: +* `add` +* `rename` +* `schedule` +* `mark` +* `unmark` +* `delete` + +Although some commands cannot be undone, you can still reverse the effect manually and easily. +* `store` - choose to `store` in your previous location again +* `load` - choose to `load` data from your previous location +* `alias` - `unalias` the shorthand command you just defined +* `unalias` - `alias` the shorthand command you just removed +* `undo` - scroll through your previous commands using the and again and enter the command to execute it again +* `list`/`find` - there is only a change in your view but no change in the task data. To go back to the previous view, use ESC + +Examples: +``` +> add homework +Result: Agendum adds the task "homework" +> undo +Result: Agendum removes the task "homework" +``` + + +``` +###### /UserGuide.md +``` md +## Conclusion +We hope that you will find Agendum and our user guide helpful. If you have any suggestions on how we can make Agendum better or improve this guide, please feel free to post on our [issue tracker](https://github.com/CS2103AUG2016-W11-C2/main/issues). + + +## Command Summary + +
+ +For a quick reference, +> * Parameters with `...` after them can have multiple instances (separated by whitespace). +> * Commands are case insensitive +> * ``, `` and `` must follow the format previously defined in [Date Time Format](#date-time-format) +``` diff --git a/collated/docs/A0148031R.md b/collated/docs/A0148031R.md new file mode 100644 index 000000000000..34d942788e1c --- /dev/null +++ b/collated/docs/A0148031R.md @@ -0,0 +1,312 @@ +# A0148031R +###### /DeveloperGuide.md +``` md +## Introduction + +Agendum is a task manager for busy users to manage their schedules and tasks via keyboard commands. It is a Java desktop application that has a **GUI** implemented with JavaFX. + +This guide describes the design and implementation of Agendum. It will help developers (like you) understand how Agendum works and how to further contribute to its development. We have organized this guide in a top-down manner so that you can understand the big picture before moving on to the more detailed sections. Each sub-section is mostly self-contained to provide ease of reference. + + +  + + +## Setting up + +### Prerequisites + +* **JDK `1.8.0_60`** or above
+ + > This application will not work with any earlier versions of Java 8. + +* **Eclipse** IDE + +* **e(fx)clipse** plugin for Eclipse (Do the steps 2 onwards given in + [this page](http://www.eclipse.org/efxclipse/install.html#for-the-ambitious)) + +* **Buildship Gradle Integration** plugin from the + [Eclipse Marketplace](https://marketplace.eclipse.org/content/buildship-gradle-integration) + + +### Importing the project into Eclipse + +1. Fork this repo, and clone the fork to your computer + +2. Open Eclipse (Note: Ensure you have installed the **e(fx)clipse** and **buildship** plugins as given in the prerequisites above) + +3. Click `File` > `Import` + +4. Click `Gradle` > `Gradle Project` > `Next` > `Next` + +5. Click `Browse`, then locate the project's directory + +6. Click `Finish` + + > * If you are asked whether to 'keep' or 'overwrite' config files, choose to 'keep'. + > * Depending on your connection speed and server load, it can even take up to 30 minutes for the set up to finish + (Gradle needs time to download library files from servers during the project set up process) + > * If Eclipse automatically changed any settings during the import process, you can discard those changes. + + > After you are done importing Agendum, it will be a good practice to enable assertions before developing. This will enable Agendum app to verify assumptions along the way. To enable assertions, follow the instructions [here](http://stackoverflow.com/questions/5509082/eclipse-enable-assertions) + +### Troubleshooting project setup + +* **Problem: Eclipse reports compile errors after new commits are pulled from Git** + * Reason: Eclipse fails to recognize new files that appeared due to the Git pull. + * Solution: Refresh the project in Eclipse:
+ +* **Problem: Eclipse reports some required libraries missing** + * Reason: Required libraries may not have been downloaded during the project import. + * Solution: [Run tests using Gardle](UsingGradle.md) once (to refresh the libraries). + + +  + + +## Design + + +``` +###### /DeveloperGuide.md +``` md +### 2. UI component + +The `UI` is the entry point of Agendum which is responsible for showing updates to the user; changes in data in the `Model` automatically updates `UI` as well. `UI` executes user commands using the Logic Component. In addition, `UI` responds to events raised from various other parts of Agendum and updates the display accordingly. + +
+ +**API** : [`Ui.java`](../src/main/java/seedu/agendum/ui/Ui.java) + +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox` and `ResultPopup`. All these, including the `MainWindow`, inherit the abstract `UiPart` class. They can be loaded using `UiPartLoader`. + +The `commandBox` component controls the field for user input, and it is associated with a `CommandBoxHistory` object which saves the most recent valid and invalid commands. `CommandBoxHistory` follows a singleton pattern to restrict the instantiation of the class to one object. + +Agendum has 3 different task panel classes `UpcomingTasksPanel`, `CompletedTaskPanel` and `FloatingTasksPanel`. They all inherit from the the `TaskPanel` class and hold and load `TaskCard` objects. + +The `UI` component uses JavaFX UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](../src/main/java/seedu/agendum/ui/MainWindow.java) is specified in + [`MainWindow.fxml`](../src/main/resources/view/MainWindow.fxml) + + +``` +###### /DeveloperGuide.md +``` md +## Dev Ops + +### 1. Build Automation + +We use Gradle to run tests and manage library dependencies. The Gradle configuration for this project is defined in _build.gradle_. + +### 2. Continuous Integration + +We use [Travis CI](https://travis-ci.org/) to perform _Continuous Integration_ on our project. When code is pushed to this repository, Travis CI will run the project tests automatically to ensure that existing functionality will not be negatively affected by the changes. + +### 3. Making a Release + +To contribute a new release: + + 1. Generate a JAR file [using Gradle](UsingGradle.md#creating-the-jar-file). + 2. Tag the repo with the version number. e.g. `v1.1` + 2. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/) + and upload the JAR file you created. + +### 4. Managing Dependencies + +Agendum depends on third-party libraries, such as the +[Jackson library](http://wiki.fasterxml.com/JacksonHome), for XML parsing, [Natty](http://natty.joestelmach.com) for date & time parsing, [Reflection](https://code.google.com/archive/p/reflections/) for examining classes at runtime and [Google Calendar SDK](https://developers.google.com/api-client-library/java/apis/calendar/v3) for sync. Managing these dependencies have been automated using Gradle. Gradle can download the dependencies automatically hence the libraries are not included in this repo and you do not need to download these libraries manually. To add a new dependency, update `build.gradle`. + + +  + + +``` +###### /DeveloperGuide.md +``` md +## Appendix B : Use Cases + +>For all use cases below, the **System** is `Agendum` and the **Actor** is the `user`, unless specified otherwise + +### Use case 01 - Add a task + +**MSS** + +1. System prompts the Actor to enter a command +2. Actor enters an add command with the task name into the input box. +3. System adds the task. +4. System shows a feedback message ("Task `name` added") and displays the updated list + Use case ends. + +**Extensions** + +2a. No task description is provided + +> 2a1. System shows an error message (“Please provide a task name/description”)
+> Use case resumes at step 1 + +2b. There is an existing task with the same description and details + +> 2b1. System shows an error message (“Please use a new task description”)
+> Use case resumes at step 1 + +### Use case 02 - Delete a task + +**MSS** + +1. Actor requests to delete a specific task in the list by its index +2. System deletes the task. +3. System shows a success feedback message to describe the task deleted and displays the updated list + Use case ends. + +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends + +### Use case 03 - Rename a task + +**MSS** + +1. Actor requests to rename a specific task in the list by its index and also input the new task name +2. System updates the task +3. System shows a success feedback message to describe the task renamed and displays the updated list + Use case ends. + +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends + +1b. No task name is provided + +> 1b1. System shows an error message to inform the user of the incorrect format/missing name +> Use case ends + +2a. Renaming a task will result in a duplicate (will become exactly identical to another task) + +> 2a1. System shows an error message to inform user of potential duplicate
+> Use case ends + +### Use case 04 - Schedule a task’s start and end time/deadline + +**MSS** + +1. Actor requests to list tasks +2. System shows a list of tasks +3. Actor inputs index and the new start/end time or deadline of the task to be modified +4. System updates the task +5. System shows a feedback message (“Task `index`'s time/date has been updated”) and displays the updated list +6. Use case ends. + +**Extensions** + +2a. The list is empty + +> Use case ends + +3a. The given index is invalid + +> 3a1. System shows an error message (“Please select a task on the list with a valid index”)
+> Use case resumes at step 2 + +3b. The new input time format is invalid + +> 3b1. System shows an error message (“Please follow the given time format”)
+> Use case resumes at step 2 + + +``` +###### /UserGuide.md +``` md +### Start using Agendum +*This is a brief introduction and suggestion on how to get started with Agendum. Refer to our [Features](#features) section, for a more extensive coverage on what Agendum can do.* + +**Step 1 - Get some help** + +Feeling lost or clueless? To see a summary of Agendum commands, use the keyboard shortcut CTRL + H to bring up the help screen as shown below. You can start typing a command and press ESC whenever you want to hide the help screen. + +**Step 2 - Add a task** + +Perhaps, you can start by adding a task to your empty Agendum to-do list. For example, you might remember you have to return your library books. Type the following line in the command box: + +`> add return library books` + +Since you did not specify a time to return the books, Agendum will add this task to the **Do It Anytime** panel. The task *return library books* has a ID *1* now. + +**Step 3 - Update your task (if needed)** + +You might change your mind and want to update the details of the task. For example, you might only want to return a single book "Animal Farm" instead. Type the following line in the command box: + +`> rename 1 return "Animal Farm"` + +Agendum will promptly update the changes. What if you suddenly discover the book is due within a week? You will want to return "Animal Farm" by Friday night. To (re)schedule the task, type the following command: + +`> schedule 1 by friday 8pm` + +Since you will have to return your books by a specific time, Agendum will move this task to the **Do It Soon** panel. + +**Step 4 - Mark a task as completed** + +With the help of Agendum, you remembered to return "Animal Farm" punctually on Friday. Record this by marking the task as completed. Type the following line in the command box: + +`> mark 1` + +Agendum will move the task _(return "Animal Farm")_ to the **Done** panel. + +**Step 5 - Good to go** + +Continue exploring Agendum. Add more tasks to your Agendum to-do list and try out the various convenient commands given in the next section. Do note that the ID of the task might change as new tasks are added, updated and marked. Agendum takes care of it for you but you should always refer to the current ID displayed. + +**Summary of all the visual changes** + +Here is a **summary of all the visual changes** you should see at every step: +
+ +From Step 4 to 5, the id of the task _return "Animal Farm"_ changed from 1 to 2. + + +  + + +``` +###### /UserGuide.md +``` md +### Searching for tasks : `find` + +As your task list grows over time, it may become harder to locate a task.
+Fortunately, Agendum can search and bring up these tasks to you (if only you remember some of the keywords):
+ +Here is the *format*: +* `find ...` - filter out all tasks containing any of the keyword(s) given + + > * The search is not case sensitive. e.g `assignment` will match `Assignment` + > * The order of the keywords does not matter. e.g. `2 essay` will match `essay 2` + > * Only the name is searched + > * Only full words will be matched e.g. `work` will not match `homework` + > * Tasks matching at least one keyword will be returned (i.e. `OR` search). e.g. If I search for `homework assignment`, I will get tasks with names that contains `homework` or `assignment` or both. + +Here is an *example*:
+
+ +Although you are looking at a narrowed down list of tasks, your data is not lost! Simply hit ESC to exit your find results and see a list of tasks. + + +### Listing all tasks : `list` + +Alternatively, after you are done searching for tasks, you can use the following command to return to the default view of all your tasks:
+The format is simply `list`. + + +``` +###### /UserGuide.md +``` md +### Viewing help : `help` + +At any point in time, if you need some reminder about the commands available, you can use the `help` command. Type `help` or use Ctrl + H to summon the help screen. To exit the help screen, use Ctrl + H again, or simply press ESC. + +Here is a tip: You can directly enter your next command too! Agendum will also exit the help screen and show your task list. + + +``` diff --git a/collated/docs/A0148095X.md b/collated/docs/A0148095X.md new file mode 100644 index 000000000000..20e55b454199 --- /dev/null +++ b/collated/docs/A0148095X.md @@ -0,0 +1,436 @@ +# A0148095X +###### /DeveloperGuide.md +``` md +### 5. Storage component + +
+ +**API** : [`Storage.java`](../src/main/java/seedu/agendum/storage/Storage.java) + +The `Storage` component has the following functions: + +* saves `UserPref` objects in json format and reads it back. +* saves the Agendum data in xml format and reads it back. + +Other components such as `Model` require the functionalities defined inside the `Storage` component in order to save task data and user preferences to the hard disk. The `Storage` component uses the *Facade* design pattern. Components such as `ModelManager` access storage sub-components through `StorageManager` which will then redirect method calls to its internal component such as `JsonUserPrefsStorage` and `XmlToDoListStorage`. `Storage` also shields the internal details of its components, such as the implementation of `XmlToDoListStorage`, from external classes. + +The Object Diagram below shows what it looks like during runtime. + +
+ +The Sequence Diagram below shows how the storage class will interact with model when `Load` command is executed. + +
+ + +### 6. Common classes + +Classes used by multiple components are in the `seedu.agendum.commons` package. + +They are further separated into sub-packages - namely `core`, `events`, `exceptions` and `util`. + +* Core - This package consists of the essential classes that are required by multiple components. +* Events -This package consists of the different type of events that can occur; these are used mainly by EventManager and EventBus. +* Exceptions - This package consists of exceptions that may occur with the use of Agendum. +* Util - This package consists of additional utilities for the different components. + + + +  + + +``` +###### /DeveloperGuide.md +``` md +## Testing + +You can find all the test files in the `./src/test/java` folder. + +### Types of Tests + +#### 1. GUI Tests + +These are _System Tests_ that test the entire App by simulating user actions on the GUI. +They are in the `guitests` package. + +#### 2. Non-GUI Tests + +These are tests that do not involve the GUI. They include, + * _Unit tests_ targeting the lowest level methods/classes.
+ e.g. `seedu.agendum.commons.StringUtilTest` tests the correctness of StringUtil methods e.g. if a source string contains a query string, ignoring letter cases. + * _Integration tests_ that are checking the integration of multiple code units + (individual code units are assumed to be working).
+ e.g. `seedu.agendum.storage.StorageManagerTest` tests if StorageManager is correctly connected to other storage components such as JsonUserPrefsStorage. + * Hybrids of _unit and integration tests_. These tests are checking multiple code units as well as + how the are connected together.
+ e.g. `seedu.agendum.logic.LogicManagerTest` will check various code units from the `Model` and `Logic` components. + +#### 3. Headless Mode GUI Tests + +Thanks to the [TestFX](https://github.com/TestFX/TestFX) library we use, +our GUI tests can be run in [headless mode](#headless-mode).
+See [UsingGradle.md](UsingGradle.md#running-tests) for instructions on how to run tests in headless mode. + +### How to Test + +#### 1. Using Eclipse + +* To run all tests, right-click on the `src/test/java` folder and choose `Run as` > `JUnit Test` +* To run a subset of tests, you can right-click on a test package, test class, or a test and choose to run as a JUnit test. + +#### 2. Using Gradle + +* Launch a terminal on Mac or command window in Windows. Navigate to Agendum’s project directory. We recommend cleaning the project before running all tests in headless mode with the following command `./gradlew clean headless allTests` on Mac and `gradlew clean headless allTests` on Windows. +* See [UsingGradle.md](UsingGradle.md) for more details on how to run tests using Gradle. + +>#### Troubleshooting tests +>**Problem: Tests fail because NullPointException when AssertionError is expected** + +>* Reason: Assertions are not enabled for JUnit tests. + This can happen if you are not using a recent Eclipse version (i.e. _Neon_ or later) +>* Solution: Enable assertions in JUnit tests as described + [here](http://stackoverflow.com/questions/2522897/eclipse-junit-ea-vm-option).
+ Delete run configurations created when you ran tests earlier. + + +  + + +``` +###### /DeveloperGuide.md +``` md +## Appendix A : User Stories + +> Priorities: +> * High (must have) - `* * *` +> * Medium (nice to have) - `* *` +> * Low (unlikely to have) - `*` + +Priority | As a ... | I want to ... | So that I can... +-------- | :-------- | :--------- | :----------- +`* * *` | User | See usage instructions | View more information about the features and commands available +`* * *` | User | Add a task | Keep track of tasks which I need work on +`* * *` | User | Add a task with start and end time | Keep track of events that need to be completed within a certain time-frame +`* * *` | User | Add a task with a deadline | Keep track of a task to be done by a specific date and time +`* * *` | User | Rename a task | update or enhance the description of a task +`* * *` | User | Edit or remove start and end time of tasks | Reschedule events with defined start and end dates +`* * *` | User | Edit or remove deadlines of tasks | Reschedule tasks which must be done by a certain date and time +`* * *` | User | Mark task(s) as completed | Keep record of tasks that have been completed without deleting, to distinguish between completed and uncompleted tasks +`* * *` | User | Unmark task(s) from completed | Update the status of my task(s) if there are new changes or I want to continue working on a recently completed task(s). +`* * *` | User | Delete task(s) | Remove task(s) that will never get done or are no longer relevant +`* * *` | User | Undo my last action(s) | Easily correct any accidental mistakes in the last command(s) +`* * *` | User | Search based on task name | Find a task without going through the entire list using a few key words. +`* * *` | User | View all my tasks | Return to the default view of task lists after I am done searching for tasks +`* * *` | User | Specify my data storage location | Easily relocate the raw file for editing and/or sync the file to a Cloud Storage service +`* * *` | User | Load from a file | Load Agendum’s task list from a certain location or a Cloud Storage service +`* * *` | User | Exit the application by typing a command | Close the app easily +`* *` | User | Filter overdue tasks and upcoming tasks (due within a week) | Decide on what needs to be done soon +`* *` | User | Filter tasks based on marked/unmarked | Review my completed tasks and decide on what I should do next +`* *` | User | Clear the command I am typing with a key | Enter a new command without having to backspace the entire command line +`* *` | Advanced user | Specify my own alias commands | Enter commands faster or change the name of a command to suit my needs +`* *` | Advanced user | Remove the alias for a command | Use it for another command alias +`* *` | Advanced user | Scroll through my past few commands | Check what I have done and redo actions easily +`* *` | Google calendar user | Sync my tasks from Agendum to Google calendar | Keep track of my tasks using both Agendum and Google Calendar +`*` | User | Add multiple time slots for a task | “Block” multiple time slots when the exact timing of a task is certain +`*` | User | Add tags for my tasks | Group tasks together and organise my task list +`*` | User | Search based on tags | Find all the tasks of a similar nature +`*` | User | Add/Remove tags for existing tasks | Update the grouping of tasks +`*` | User | Be notified of deadline/time clashes | Resolve these conflicts manually +`*` | User | Key in emojis/symbols and characters from other languages e.g. Mandarin | Capture information in other languages +`*` | User | Clear all existing tasks | Easily start afresh with a new task list +`*` | User | See the count/statistics for upcoming/ overdue and pending tasks | Know how many tasks I need to do +`*` | User | Sort tasks by alphabetical order and date | Organise and easily locate tasks +`*` | Advanced user | Import tasks from an existing text file | Add multiple tasks efficiently without relying on multiple commands +`*` | Advanced user | Save a backup of the application in a custom file | Restore it any time at a later date +`*` | Busy user | Add recurring events or tasks | Keep the same tasks in my task list without adding them manually +`*` | Busy User | Search for tasks by date (e.g. on/before a date) | Easily check my schedule and make plans accordingly +`*` | Busy User | Search for a time when I am free | Find a suitable slot to schedule an item +`*` | Busy user | Can specify a priority of a task | Keep track of what tasks are more important + + +  + + +``` +###### /DeveloperGuide.md +``` md +### Use case 08 - Add alias commands + +**MSS** + +1. Actor enters a alias command and specify the name and new alias name of the command +2. System alias the command +3. System shows a feedback message (“The command `original command` can now be keyed in as `alias key`”) +4. Use case ends. + +**Extensions** + +1a. There is no existing command with the original name specified + +> 1a1. System shows an error message (“There is no such existing command”)
+> Use case ends + +1b. The new alias name is already reserved/used for other commands + +> 1b1. System shows an error message ("The alias `alias key` is already in use")
+> Use case ends + + +### Use case 09 - Remove alias commands + +**MSS** + +1. Actor enters the unalias command followed by `alias key` +2. System removes the alias for the command +3. System shows a feedback message ("The alias `alias key` for `original command` has been removed.") +4. Use case ends. + +**Extensions** + +1a. There is no existing alias +> 1a1. System shows an error message (“There is no such existing alias”)
+> Use case ends + + +### Use case 10 - Specify data storage location + +**MSS** + +1. Actor enters store command followed by a path to file +2. System updates data storage location to the specified path to file +3. System saves task list to the new data storage location +4. System shows a feedback message ("New save location: `location`") +5. Use case ends. + +**Extensions** + +1a. Path to file is input as 'default' +> 1a1. System updates data storage location to default
+> 1a2. System shows a feedback message ("Save location set to default: `location`")
+> Use case ends + +1b. File exists +> 1b1. System shows an error message ("The specified file exists; would you like to use LOAD instead?")
+> Use case ends + +1c. Path to file is in the wrong format +> 1c1. System shows an error message ("The specified path is in the wrong format. Example: store agendum/todolist.xml")
+> Use case ends + +1d. Path to file is not accessible +> 1d1. System shows an error message ("The specified location is inaccessible; try running Agendum as administrator.")
+> Use case ends + + +### Use case 11 - Load from data file + +**MSS** + +1. Actor enters load command followed by a path to file +2. System saves current task list into existing data storage location +3. System loads task list from specified path to file +2. System updates data storage location to the specified path to file +3. System shows a feedback message ("Data successfully loaded from: `location`") +4. Use case ends. + +**Extensions** + +1a. Path to file is invalid +> 1a1. System shows an error message ("The specified path to file is invalid: `location`")
+> Use case ends + +2a. File does not exist +> 1a1. System shows an error message ("The specified file does not exist: `location`")
+> Use case ends + +3a. File is in the wrong format +> 3a1. System shows an error message ("File is in the wrong format.")
+> Use case ends + + +  + + +``` +###### /DeveloperGuide.md +``` md +## Appendix D : Glossary + +##### Mainstream OS: + +Windows, Linux, Unix, OS-X + +##### Headless Mode: + +In the headless mode, GUI tests do not show up on the screen.
+This means you can do other things on the Computer while the tests are running. + + +  + + +``` +###### /UserGuide.md +``` md +## Introduction +Hi there! Do you have too many tasks to do? Are you unable to keep track of all of them? Are you looking for a hassle-free task manager which will work swiftly? + +Enter Agendum. + +This task manager will assist you in completing all your tasks on time. It will automatically sort your tasks by date so you can always see the most urgent tasks at the top of the list! + +Agendum is simple, flexible and phenomenally keyboard friendly. With just one line of command, Agendum will carry out your wishes. You don’t ever have to worry about having to click multiple buttons and links. Agendum is even capable of supporting your own custom command words! This means that you can get things done even faster, your way. + + +  + + +``` +###### /UserGuide.md +``` md +### Creating an alias for a command : `alias` + +Perhaps you want to type a command faster, or change the name of a command to suit your needs;
+fret not, Agendum allows you to define your own aliases for commands.
+You can use both the original command and your own shorthand alias to carry out the same action. + +To create an alias, here is the *format*: +* `alias ` + +> * `` must be a single alphanumeric word. It cannot be a original-command or already aliased to another command. +> * `` must be a command word that is specified in the Command Summary section + +Examples: +``` +> alias mark m +Result: you can now use `m` or `mark` to mark a task as completed. +> alias mark mk +Result: Now you can use "m", "mk" or "mark" to mark a task as completed. +``` + + +### Removing an alias command : `unalias` + +Is a current alias inconvenient? Have you thought of a better one?
+Or perhaps you are thinking of using an alias for another command instead.
+ +To remove a previously defined alias, here is the *format*: +* `unalias ` + +> * `` should be an alias you previously defined. +> * After removing this particular alias, you can still use the original command word or other unremoved aliases. + +Examples: +``` +If mark is aliased with "m" and "mk". +> unalias mk +Result: "mk" can no longer be used to mark tasks; now you can only use the +original command "mark" or "m" to mark a task as completed. +``` + + +``` +###### /UserGuide.md +``` md +### Specifying the data storage location : `store` + +Are you considering moving Agendum’s data files to another file directory? +You might want to save your Agendum task list to a Cloud Storage service so you can easily access from another device. +Agendum offers you the flexibility in choosing where the task list data will be stored. +The task list data will be saved to the specific directory and future data will be saved in that location. + +Here is the *format*: +* `store ` + +> * `` must be a valid path to a file on the local computer. +> * If there is an existing file at ``, it will be overriden. +> * The data storage file at the original location will not be deleted. +> * This command is similar to a "Save as..." in other applications. + +Examples: +``` +> store C:/Dropbox/mytasklist.xml +``` + + +### Loading from another data storage location : `load` + +After relocating Agendum’s data files, you might want to load that exact copy of Agendum’s task list from a certain location, or from a Cloud Storage service. Agendum also offers you the flexibility to choose which data files to import. + +Here is the *format*: +* `load ` + +> * `` must be a valid path to a file on the local computer. +> * Your current data would have already been saved automatically in its original data storage location. +> * Agendum will then show data loaded from `` and save data there in the future. +> * You will not be able to `undo` immediately after loading as there have been no changes to the loaded list. + +Examples +``` +> load C:/Dropbox/mytasklist.xml +``` + +### Synchronizing with Google calendar: `sync` + +If you have a Google account and want to synchronize your tasks from Agendum to Google Calendar, this command enables you to do exactly that! Synchronization takes place when you turn it on. + +Here is the *format*: +* `sync ON` or `sync OFF` + +> * `sync` must have either ON or OFF after the command word +> * Only data from Agendum will be synchronized to Google Calendar +> * Only tasks with a start and end date/time will be synchronized +> * Please accept or decline Google's request for permission for Agendum client to manage your calendar. Do not close the window abruptly. + + +### Exiting Agendum : `exit` + +Are you done with organizing your tasks? Well done!
+To leave Agendum, type `exit`. See you soon! + + +### Keyboard Shortcuts + +To work even faster you can also use keyboard shortcuts:
+1. Use and to scroll through previously typed commands. You don't need to remember or enter them again!
+2. If you are entering a new command, use to instantly clear the command line and start afresh.
+3. Use Tab to quickly auto-complete a command word when you are typing.
+4. Use Ctrl + H to conveniently switch between the help window and the command box. Also, you can use ESC to close help window.
+5. Use Ctrl + Z in place of `undo` + + +  + + +## FAQ + + +
+
Q: How do I save my task data in Agendum?
+
Agendum saves your data automatically whenever your task list is updated. There is no need to save manually. Agendum will save the data at the speicified storage location. By default, it will save to `data/todolist.xml`
+ +
Q: How do I transfer my data to another computer?
+
Firstly, note down the current save location of Agendum's task data (which is displayed in the bottom status bar). In your file directory, navigate to this location and copy the data file to a portable USB device, hard disk or your cloud storage folder. Alternatively, you can make use of the store command to transfer the file within Agendum. Then, ensure that you have installed Agendum in the other computer. Copy the data file from your device onto the other computer, preferrably in the same folder as Agendum. Use the load command to load it into Agendum.
+ +
Q: Why did Agendum complain about an invalid file directory?
+
Check if the directory you wish to relocate to exists and if you have enough administrator privileges.
+ +
Q: Can Agendum remind me when my task is due soon?
+
Agendum will always show the tasks that are due soon at the top of list. However, Agendum will not show you a reminder (yet).
+ +
Q: Why did Agendum complain that the task already exists?
+
You have previously created a task with the same name, start and end time. The tasks have the same completion status too! Save the trouble of creating one or it will be helpful to distinguish them by renaming instead. + +
Q: Why did Agendum reject my alias for a command?
+
The short-hand command cannot be one of Agendum’s command keywords (e.g. add, delete) and cannot be concurrently used to alias another command (e.g. m cannot be used for both mark and unmark).
+ +
Q: I can't launch Agendum. What is wrong?
+
Check if the config file in data/json/config.json contains the correct file paths to other data such as your to-do list. It might be helpful to delete the user preferences file.
+ +
+ + +  + + +``` diff --git a/collated/main/A0003878Y.md b/collated/main/A0003878Y.md new file mode 100644 index 000000000000..31a12acc997d --- /dev/null +++ b/collated/main/A0003878Y.md @@ -0,0 +1,911 @@ +# A0003878Y +###### /java/seedu/agendum/logic/commands/AddCommand.java +``` java + /** + * Convenience constructor using name + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public AddCommand(String name) + throws IllegalValueException { + this.toAdd = new Task( + new Name(name) + ); + } + + /** + * Convenience constructor using name, end datetime + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public AddCommand(String name, Optional deadlineDate) + throws IllegalValueException { + this.toAdd = new Task( + new Name(name), + deadlineDate + ); + } + + /** + * Convenience constructor using name, start datetime, end datetime + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public AddCommand(String name, Optional startDateTime, Optional endDateTime) + throws IllegalValueException { + Optional balancedEndDateTime = endDateTime; + if (startDateTime.isPresent() && endDateTime.isPresent()) { + balancedEndDateTime = Optional.of(DateTimeUtils.balanceStartAndEndDateTime(startDateTime.get(), endDateTime.get())); + } + this.toAdd = new Task( + new Name(name), + startDateTime, + balancedEndDateTime + ); + } + + @Override + public CommandResult execute() { + assert model != null; + try { + model.addTask(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } catch (UniqueTaskList.DuplicateTaskException e) { + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } + } + +``` +###### /java/seedu/agendum/logic/commands/CommandLibrary.java +``` java + private CommandLibrary() { + allCommandWords = new Reflections("seedu.agendum").getSubTypesOf(Command.class) + .stream() + .map(s -> { + try { + return s.getMethod("getName").invoke(null).toString(); + } catch (NullPointerException e) { + return null; + } catch (Exception e) { + logger.severe("Java reflection for Command class failed"); + throw new RuntimeException(); + } + }) + .filter(p -> p != null) // remove nulls + .collect(Collectors.toList()); + } + + //@author + + public static CommandLibrary getInstance() { + return commandLibrary; + } + + public Hashtable getAliasTable() { + return aliasTable; + } + +``` +###### /java/seedu/agendum/logic/commands/ScheduleCommand.java +``` java + public ScheduleCommand(int targetIndex, Optional startTime, + Optional endTime) { + Optional balancedEndTime = endTime; + if (startTime.isPresent() && endTime.isPresent()) { + balancedEndTime = Optional.of(DateTimeUtils.balanceStartAndEndDateTime(startTime.get(), endTime.get())); + } + this.targetIndex = targetIndex; + this.newStartDateTime = startTime; + this.newEndDateTime = balancedEndTime; + } + + @Override + public CommandResult execute() { + assert model != null; + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + ReadOnlyTask taskToSchedule = lastShownList.get(targetIndex - 1); + + Task updatedTask = new Task(taskToSchedule); + updatedTask.setStartDateTime(newStartDateTime); + updatedTask.setEndDateTime(newEndDateTime); + + try { + model.updateTask(taskToSchedule, updatedTask); + } catch (UniqueTaskList.DuplicateTaskException e) { + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } catch (TaskNotFoundException e) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, updatedTask)); + } + +``` +###### /java/seedu/agendum/logic/commands/SyncCommand.java +``` java + /** + * Convenience constructor using name + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public SyncCommand(String option) throws IllegalValueException { + + if (option.trim().equalsIgnoreCase(SYNC_ON)) { + syncOption = true; + } else if (option.trim().equalsIgnoreCase(SYNC_OFF)) { + syncOption = false; + } else { + throw new IllegalValueException(MESSAGE_WRONG_OPTION); + } + } + + @Override + public CommandResult execute() { + if (syncOption) { + model.activateModelSyncing(); + return new CommandResult(SYNC_ON_MESSAGE); + } else { + model.deactivateModelSyncing(); + return new CommandResult(SYNC_OFF_MESSAGE); + } + } + +``` +###### /java/seedu/agendum/logic/parser/DateTimeUtils.java +``` java + +/** + * Utilities for DateTime parsing + */ +public class DateTimeUtils { + + /** + * Parses input string into LocalDateTime objects using Natural Language Parsing + * @param input natural language date time string + * @return Optional is null if input coult not be parsed + */ + public static Optional parseNaturalLanguageDateTimeString(String input) { + if(input == null || input.isEmpty()) { + return Optional.empty(); + } + // Referring to natty's Parser Class using its full path because of the namespace collision with our Parser class. + com.joestelmach.natty.Parser parser = new com.joestelmach.natty.Parser(); + List groups = parser.parse(input); + + if (groups.size() <= 0) { + // Nothing found + return Optional.empty(); + } + + DateGroup dateGroup = (DateGroup) groups.get(0); + + if (dateGroup.getDates().size() < 0) { + return Optional.empty(); + } + + Date date = dateGroup.getDates().get(0); + + LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + return Optional.ofNullable(localDateTime); + } + + /** + * Takes two LocalDateTime and balances by ensuring that the latter DateTime is gaurenteed to be later + * than the former DateTime + * @param startDateTime + * @param endDateTime + * @return endDateTime that is now balanced + */ + public static LocalDateTime balanceStartAndEndDateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) { + LocalDateTime newEndDateTime = endDateTime; + while (startDateTime.compareTo(newEndDateTime) >= 1) { + newEndDateTime = newEndDateTime.plusDays(1); + } + return newEndDateTime; + } + + public static boolean containsTime(String input) { + return parseNaturalLanguageDateTimeString(input).isPresent(); + } +} +``` +###### /java/seedu/agendum/logic/parser/EditDistanceCalculator.java +``` java + +/** + * A static class for calculating levenshtein distance between two strings + */ +public class EditDistanceCalculator { + + private static final Logger logger = LogsCenter.getLogger(EditDistanceCalculator.class); + private static final int EDIT_DISTANCE_THRESHOLD = 3; + + /** + * Attempts to find the 'closest' command for an input String + * @param input user inputted command + * @return Optional string that's the closest command to input. Null if not found. + */ + public static Optional closestCommandMatch(String input) { + final String[] bestCommand = {""}; + final int[] bestCommandDistance = {Integer.MAX_VALUE}; + + Consumer consumer = (commandWord) -> { + int commandWordDistance = distance(input, commandWord); + + if (commandWordDistance < bestCommandDistance[0]) { + bestCommand[0] = commandWord; + bestCommandDistance[0] = commandWordDistance; + } + }; + executeOnAllCommands(consumer); + + if (bestCommandDistance[0] < EDIT_DISTANCE_THRESHOLD) { + return Optional.of(bestCommand[0]); + } else { + return Optional.empty(); + } + } + + /** + * Attempts to 'complete' the input String into an actual command + * @param input user inputted command + * @return Optional string that's command that best completes the input. If input matches more than + * one command, null ire returned. Null is also returned if a command is not found. + */ + public static Optional findCommandCompletion(String input) { + ArrayList matchedCommands = new ArrayList<>(); + + Consumer consumer = (commandWord) -> { + if (commandWord.startsWith(input)) { + matchedCommands.add(commandWord); + } + }; + executeOnAllCommands(consumer); + + if (matchedCommands.size() == 1) { + return Optional.of(matchedCommands.get(0)); + } else { + return Optional.empty(); + } + } + + /** + * A higher order method that takes in an operation to perform on all Commands using + * Java reflection and functional programming paradigm. + * @param f A closure that takes a String as input that executes on all Commands. + */ + private static void executeOnAllCommands(Consumer f) { + new Reflections("seedu.agendum").getSubTypesOf(Command.class) + .stream() + .map(s -> { + try { + return s.getMethod("getName").invoke(null).toString(); + } catch (NullPointerException e) { + return ""; // Suppress this exception are we expect some Commands to not conform to getName() + } catch (Exception e) { + logger.severe("Java reflection for Command class failed"); + throw new RuntimeException(); + } + }) + .filter(p -> p != "") // remove empty + .forEach(f); // execute given lambda on each nonnull String. + } + + + /** + * Calculates levenshtein distnace between two strings. + * Code from https://rosettacode.org/wiki/Levenshtein_distance#Java + * @param a + * @param b + * @return + */ + private static int distance(String a, String b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + int [] costs = new int [b.length() + 1]; + for (int j = 0; j < costs.length; j++) + costs[j] = j; + for (int i = 1; i <= a.length(); i++) { + costs[0] = i; + int nw = i - 1; + for (int j = 1; j <= b.length(); j++) { + int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1); + nw = costs[j]; + costs[j] = cj; + } + } + return costs[b.length()]; + } + +} +``` +###### /java/seedu/agendum/logic/parser/Parser.java +``` java + private static final Pattern QUOTATION_FORMAT = Pattern.compile("\'([^\']*)\'"); + private static final Pattern ADD_SCHEDULE_ARGS_FORMAT = Pattern.compile("(?:.+?(?=(?:(?:(?i)by|from|to)\\s|$)))+?"); + + private static final String ARGS_FROM = "from"; + private static final String ARGS_BY = "by"; + private static final String ARGS_TO = "to"; + private static final String FILLER_WORD = "FILLER "; + private static final String SINGLE_QUOTE = "\'"; + + private static final String[] TIME_TOKENS = new String[] { ARGS_FROM, ARGS_TO, ARGS_BY }; + + private CommandLibrary commandLibrary; + +``` +###### /java/seedu/agendum/logic/parser/Parser.java +``` java + Optional alternativeCommand = EditDistanceCalculator.closestCommandMatch(commandWord); + if (alternativeCommand.isPresent()) { + return new IncorrectCommand(String.format(MESSAGE_UNKNOWN_COMMAND_WITH_SUGGESTION, alternativeCommand.get())); + } else { + return new IncorrectCommand(MESSAGE_UNKNOWN_COMMAND); + } + } + } + +``` +###### /java/seedu/agendum/logic/parser/Parser.java +``` java + /** + * Parses arguments in the context of the add task command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareAdd(String args) { + + // Create title and dateTimeMap + StringBuilder titleBuilder = new StringBuilder(); + HashMap> dateTimeMap = new HashMap<>(); + + // Check for quotation in args. If so, they're set as title + Optional quotationCheck = checkForQuotation(args); + if (quotationCheck.isPresent()) { + titleBuilder.append(quotationCheck.get().replace(SINGLE_QUOTE,"")); + args = FILLER_WORD + args.replace(quotationCheck.get(),""); // This will get removed later by regex + } + + // Start parsing for datetime in args + Matcher matcher = ADD_SCHEDULE_ARGS_FORMAT.matcher(args.trim()); + + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + try { + matcher.reset(); + matcher.find(); + if (titleBuilder.length() == 0) { + titleBuilder.append(matcher.group(0)); + } + + // Run this function on all matched groups + BiConsumer consumer = (matchedGroup, token) -> { + String time = matchedGroup.substring(token.length(), matchedGroup.length()); + if (DateTimeUtils.containsTime(time)) { + dateTimeMap.put(token, DateTimeUtils.parseNaturalLanguageDateTimeString(time)); + } else { + titleBuilder.append(matchedGroup); + } + }; + executeOnEveryMatcherToken(matcher, consumer); + + String title = titleBuilder.toString(); + + if (title.length() == 0) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + boolean hasDeadlineKeyword = dateTimeMap.containsKey(ARGS_BY); + boolean hasStartTimeKeyword = dateTimeMap.containsKey(ARGS_FROM); + boolean hasEndTimeKeyword = dateTimeMap.containsKey(ARGS_TO); + + if (hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new AddCommand(title, dateTimeMap.get(ARGS_BY)); + } + + if (!hasDeadlineKeyword && hasStartTimeKeyword && hasEndTimeKeyword) { + return new AddCommand(title, dateTimeMap.get(ARGS_FROM), dateTimeMap.get(ARGS_TO)); + } + + if (!hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new AddCommand(title); + } + + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } catch (IllegalValueException ive) { + return new IncorrectCommand(ive.getMessage()); + } + } + + + /** + * Parses arguments in the context of the schedule task command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareSchedule(String args) { + Matcher matcher = ADD_SCHEDULE_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ScheduleCommand.MESSAGE_USAGE)); + } + + matcher.reset(); + matcher.find(); + HashMap> dateTimeMap = new HashMap<>(); + Optional taskIndex = parseIndex(matcher.group(0)); + int index = 0; + if (taskIndex.isPresent()) { + index = taskIndex.get(); + } else { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ScheduleCommand.MESSAGE_USAGE)); + } + + // Run this function on all matched groups + BiConsumer consumer = (matchedGroup, token) -> { + String time = matchedGroup.substring(token.length(), matchedGroup.length()); + if (DateTimeUtils.containsTime(time)) { + dateTimeMap.put(token, DateTimeUtils.parseNaturalLanguageDateTimeString(time)); + } + }; + executeOnEveryMatcherToken(matcher, consumer); + + boolean hasDeadlineKeyword = dateTimeMap.containsKey(ARGS_BY); + boolean hasStartTimeKeyword = dateTimeMap.containsKey(ARGS_FROM); + boolean hasEndTimeKeyword = dateTimeMap.containsKey(ARGS_TO); + + if (hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new ScheduleCommand(index, Optional.empty(), dateTimeMap.get(ARGS_BY)); + } + + if (!hasDeadlineKeyword && hasStartTimeKeyword && hasEndTimeKeyword) { + return new ScheduleCommand(index, dateTimeMap.get(ARGS_FROM), dateTimeMap.get(ARGS_TO));} + + if (!hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new ScheduleCommand(index, Optional.empty(), Optional.empty()); + } + + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE)); + } + + /** + * Checks if there are any quotation marks in the given string + * + * @param str + * @return returns the string inside the quote. + */ + private Optional checkForQuotation(String str) { + Matcher matcher = QUOTATION_FORMAT.matcher(str.trim()); + if (!matcher.find()) { + return Optional.empty(); + } + return Optional.of(matcher.group(0)); + } + + /** + * A higher order function that parses arguments in the context of the schedule task command. + * Extracted out of prepareAdd and prepareSchedule for code reuse. + * + * @param matcher matcher for current command context + * @param consumer closure to execute on + */ + private void executeOnEveryMatcherToken(Matcher matcher, BiConsumer consumer) { + while (matcher.find()) { + for (String token : TIME_TOKENS) { + String matchedGroup = matcher.group(0).toLowerCase(); + if (matchedGroup.startsWith(token)) { + consumer.accept(matchedGroup, token); + } + } + } + } + + +``` +###### /java/seedu/agendum/logic/parser/Parser.java +``` java + /** + * Parses arugments in the context of the sync command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareSync(String args) { + try { + return new SyncCommand(args); + } catch (IllegalValueException ive) { + return new IncorrectCommand(ive.getMessage()); + } + } + +``` +###### /java/seedu/agendum/model/ModelManager.java +``` java + + //=========== Sync Methods =============================================================================== + + @Override + public void activateModelSyncing() { + if (syncManager.getSyncStatus() != Sync.SyncStatus.RUNNING) { + syncManager.startSyncing(); + + // Add all current events into sync provider + mainToDoList.getTasks().forEach(syncManager::addNewEvent); + } + } + + @Override + public void deactivateModelSyncing() { + if (syncManager.getSyncStatus() != Sync.SyncStatus.NOTRUNNING) { + syncManager.stopSyncing(); + } + } + + + +``` +###### /java/seedu/agendum/sync/Sync.java +``` java +public interface Sync { + + /** Enum used to persist SyncManager status **/ + enum SyncStatus { + RUNNING, NOTRUNNING + } + + /** Retrieve sync manager sync status **/ + SyncStatus getSyncStatus(); + + /** Sets sync manager sync status **/ + void setSyncStatus(SyncStatus syncStatus); + + /** Turn on syncing **/ + void startSyncing(); + + /** Turn off syncing **/ + void stopSyncing(); + + /** Add Task to sync provider **/ + void addNewEvent(Task task); + + /** Remove task from sync provider **/ + void deleteEvent(Task task); +} +``` +###### /java/seedu/agendum/sync/SyncManager.java +``` java +public class SyncManager extends ComponentManager implements Sync { + private final Logger logger = LogsCenter.getLogger(SyncManager.class); + private SyncStatus syncStatus = SyncStatus.NOTRUNNING; + + private final SyncProvider syncProvider; + + public SyncManager(SyncProvider syncProvider) { + this.syncProvider = syncProvider; + this.syncProvider.setManager(this); + + syncProvider.startIfNeeded(); + } + + @Override + public SyncStatus getSyncStatus() { + return syncStatus; + } + + @Override + public void setSyncStatus(SyncStatus syncStatus) { + this.syncStatus = syncStatus; + } + + @Override + public void startSyncing() { + syncProvider.start(); + } + + @Override + public void stopSyncing() { + syncProvider.stop(); + } + + @Override + public void addNewEvent(Task task) { + if (syncStatus == SyncStatus.RUNNING) { + if (task.getStartDateTime().isPresent() && task.getEndDateTime().isPresent()) { + syncProvider.addNewEvent(task); + } + } + } + + @Override + public void deleteEvent(Task task) { + if (syncStatus == SyncStatus.RUNNING) { + syncProvider.deleteEvent(task); + } + } +} +``` +###### /java/seedu/agendum/sync/SyncProvider.java +``` java +public abstract class SyncProvider { + + /** Sync provider's keep a reference to the manager so that they can set it's + * sync status **/ + protected Sync syncManager; + + /** Start sync provider and perform initialization **/ + public abstract void start(); + + /** Start sync provider if it needs to be started **/ + public abstract void startIfNeeded(); + + /** Stop sync provider and perform cleanup **/ + public abstract void stop(); + + /** Add event into sync provider **/ + public abstract void addNewEvent(Task task); + + /** Delete event from sync provider **/ + public abstract void deleteEvent(Task task); + + /** Set sync provider's sync manager **/ + public void setManager(Sync syncManager) { + this.syncManager = syncManager; + } +} +``` +###### /java/seedu/agendum/sync/SyncProviderGoogle.java +``` java +public class SyncProviderGoogle extends SyncProvider { + private final Logger logger = LogsCenter.getLogger(SyncProviderGoogle.class); + + private static final String CALENDAR_NAME = "Agendum Calendar"; + private static final File DATA_STORE_DIR = new File(DEFAULT_DATA_DIR); + private static final File DATA_STORE_CREDENTIAL = new File(DEFAULT_DATA_DIR + "StoredCredential"); + private static final String CLIENT_ID = "1011464737889-n9avi9id8fur78jh3kqqctp9lijphq2n.apps.googleusercontent.com"; + private static final String CLIENT_SECRET = "ea78y_rPz3G4kwIV3yAF99aG"; + private static FileDataStoreFactory dataStoreFactory; + private static HttpTransport httpTransport; + private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + private static com.google.api.services.calendar.Calendar client; + + private Calendar agendumCalendar; + + // These are blocking queues to ease the producer/consumer problem + private static final ArrayBlockingQueue addEventConcurrentQueue = new ArrayBlockingQueue(200); + private static final ArrayBlockingQueue deleteEventConcurrentQueue = new ArrayBlockingQueue(200); + + public SyncProviderGoogle() { + try { + httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR); + } catch (IOException var3) { + System.err.println(var3.getMessage()); + } catch (Throwable var4) { + var4.printStackTrace(); + } + } + + @Override + public void start() { + logger.info("Initializing Google Calendar Sync"); + try { + Credential t = authorize(); + client = (new com.google.api.services.calendar.Calendar.Builder(httpTransport, JSON_FACTORY, t)).setApplicationName("Agendum").build(); + agendumCalendar = getAgendumCalendar(); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + + // Process add & delete consumers into their own separate thread. + Executors.newSingleThreadExecutor().execute(() -> processAddEventQueue()); + Executors.newSingleThreadExecutor().execute(() -> processDeleteEventQueue()); + } catch (IOException var3) { + System.err.println(var3.getMessage()); + } catch (Throwable var4) { + var4.printStackTrace(); + } + } + + @Override + public void startIfNeeded() { + logger.info("Checking if Google Calendar needs to be started"); + if (DATA_STORE_CREDENTIAL.exists()) { + logger.info("Credentials, starting Google Calendar"); + start(); + } + } + + @Override + public void stop() { + logger.info("Stopping Google Calendar Sync"); + DATA_STORE_CREDENTIAL.delete(); + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + } + + @Override + public void addNewEvent(Task task) { + try { + addEventConcurrentQueue.put(task); + logger.info("Task added to GCal add queue"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public void deleteEvent(Task task) { + try { + deleteEventConcurrentQueue.put(task); + logger.info("Task added to GCal delete queue"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Authorize with Google Calendar + * @return Credentail + * @throws Exception + */ + private Credential authorize() throws Exception { + GoogleClientSecrets.Details details = new GoogleClientSecrets.Details(); + details.setClientId(CLIENT_ID); + details.setClientSecret(CLIENT_SECRET); + + GoogleClientSecrets clientSecrets = new GoogleClientSecrets().setInstalled(details); + + GoogleAuthorizationCodeFlow flow = (new GoogleAuthorizationCodeFlow.Builder(httpTransport, JSON_FACTORY, clientSecrets, Collections.singleton("https://www.googleapis.com/auth/calendar"))).setDataStoreFactory(dataStoreFactory).build(); + return (new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver())).authorize("user"); + } + + /** + * Returns a new "Agendum Calendar" in the authenticated user. + * If a calendar with the same name doesn't already exist, it creates one. + * @return + * @throws IOException + */ + private Calendar getAgendumCalendar() throws IOException { + CalendarList feed = client.calendarList().list().execute(); + logger.info("Searching for Agnendum Calendar"); + + for (CalendarListEntry entry : feed.getItems()) { + if (entry.getSummary().equals(CALENDAR_NAME)) { + logger.info(CALENDAR_NAME + " found"); + Calendar calendar = client.calendars().get(entry.getId()).execute(); + logger.info(calendar.toPrettyString()); + return calendar; + } + + } + + logger.info(CALENDAR_NAME + " not found, creating it"); + Calendar entry = new Calendar(); + entry.setSummary(CALENDAR_NAME); + Calendar calendar = client.calendars().insert(entry).execute(); + logger.info(calendar.toPrettyString()); + return calendar; + } + + /** + * Delete Agendum calendar in Google Calendar. + */ + public void deleteAgendumCalendar() { + try { + CalendarList feed = client.calendarList().list().execute(); + logger.info("Deleting Agendum calendar"); + + for (CalendarListEntry entry : feed.getItems()) { + if (entry.getSummary().equals(CALENDAR_NAME)) { + client.calendars().delete(entry.getId()).execute(); + } + + } + } catch (IOException e) + {e.printStackTrace(); + } + } + + /** + * A event loop that continuously processes the add event queue. + * + * `.take();` is a blocking call so it waits until there is something + * in the array before returning. + * + * This method should only be called on non-main thread. + */ + private void processAddEventQueue() { + while (true) { + try { + Task task = addEventConcurrentQueue.take(); + Date startDate = Date.from(task.getStartDateTime().get().atZone(ZoneId.systemDefault()).toInstant()); + Date endDate = Date.from(task.getEndDateTime().get().atZone(ZoneId.systemDefault()).toInstant()); + String id = Integer.toString(abs(task.syncCode())); + + EventDateTime startEventDateTime = new EventDateTime().setDateTime(new DateTime(startDate)); + EventDateTime endEventDateTime = new EventDateTime().setDateTime(new DateTime(endDate)); + + Event newEvent = new Event(); + newEvent.setSummary(String.valueOf(task.getName())); + newEvent.setStart(startEventDateTime); + newEvent.setEnd(endEventDateTime); + newEvent.setId(id); + + Event result = client.events().insert(agendumCalendar.getId(), newEvent).execute(); + logger.info(result.toPrettyString()); + + logger.info("Task processed from GCal add queue"); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * A event loop that continuously processes the delete event queue. + * + * `.take();` is a blocking call so it waits until there is something + * in the array before returning. + * + * This method should only be called on non-main thread. + */ + private void processDeleteEventQueue() { + while (true) { + try { + Task task = deleteEventConcurrentQueue.take(); + String id = Integer.toString(abs(task.syncCode())); + client.events().delete(agendumCalendar.getId(), id).execute(); + + logger.info("Task added to GCal delete queue"); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} +``` +###### /java/seedu/agendum/ui/HelpWindow.java +``` java + + /** + * Uses Java reflection followed by Java stream.map() to retrieve all commands for listing on the Help + * window dynamically + */ + private void loadHelpList() { + new Reflections("seedu.agendum").getSubTypesOf(Command.class) + .stream() + .map(s -> { + try { + Map map = new HashMap<>(); + map.put(CommandColumns.COMMAND, s.getMethod("getName").invoke(null).toString()); + map.put(CommandColumns.FORMAT, s.getMethod("getFormat").invoke(null).toString()); + map.put(CommandColumns.DESCRIPTION, s.getMethod("getDescription").invoke(null).toString()); + return map; + } catch (NullPointerException e) { + return null; // Suppress this exception are we expect some Commands to not conform to these methods + } catch (Exception e) { + logger.severe("Java reflection for Command class failed"); + throw new RuntimeException(); + } + }) + .filter(p -> p != null) // remove nulls + .sorted((lhs, rhs) -> lhs.get(CommandColumns.COMMAND).compareTo(rhs.get(CommandColumns.COMMAND))) + .forEach(m -> commandList.add(m)); + } +} +``` diff --git a/collated/main/A0133367E.md b/collated/main/A0133367E.md new file mode 100644 index 000000000000..a28c6f515b95 --- /dev/null +++ b/collated/main/A0133367E.md @@ -0,0 +1,1371 @@ +# A0133367E +###### /java/seedu/agendum/commons/events/logic/AliasTableChangedEvent.java +``` java +package seedu.agendum.commons.events.logic; + +import java.util.Hashtable; + +import seedu.agendum.commons.events.BaseEvent; + +/** + * Indicate the alias table in {@link seedu.agendum.logic.commands.CommandLibrary} has changed + */ +public class AliasTableChangedEvent extends BaseEvent { + + public final Hashtable aliasTable; + private String message_; + + public AliasTableChangedEvent(String message, Hashtable aliasTable) { + this.aliasTable = aliasTable; + this.message_ = message; + } + + @Override + public String toString() { + return message_; + } +} +``` +###### /java/seedu/agendum/logic/commands/AliasCommand.java +``` java +/** + * Creates an alias for a reserved command keyword + */ +public class AliasCommand extends Command { + + public static final String COMMAND_WORD = "alias"; + public static final String COMMAND_FORMAT = "alias "; + public static final String COMMAND_DESCRIPTION = "create your shorthand command"; + public static final String MESSAGE_SUCCESS = "New alias <%1$s> created for <%2$s>"; + public static final String MESSAGE_FAILURE_ALIAS_IN_USE = "<%1$s> is already an alias for <%2$s>"; + public static final String MESSAGE_FAILURE_RESERVED_COMMAND_WORD = "<%1$s> is a reserved command word"; + public static final String MESSAGE_FAILURE_NON_ORIGINAL_COMMAND = + "We don't recognise <%1$s> as an Agendum Command"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " mark m"; + + private String aliasValue; + private String aliasKey; + private CommandLibrary commandLibrary; + + public AliasCommand(String aliasKey, String aliasValue) { + this.aliasKey = aliasKey; + this.aliasValue = aliasValue; + } + + public void setData(Model model, CommandLibrary commandLibrary) { + this.model = model; + this.commandLibrary = commandLibrary; + } + + @Override + public CommandResult execute() { + if (commandLibrary.isReservedCommandKeyword(aliasKey)) { + return new CommandResult(String.format( + MESSAGE_FAILURE_RESERVED_COMMAND_WORD, aliasKey)); + } + + if (commandLibrary.isExistingAliasKey(aliasKey)) { + String associatedValue = commandLibrary.getAliasedValue(aliasKey); + return new CommandResult(String.format( + MESSAGE_FAILURE_ALIAS_IN_USE, aliasKey, associatedValue)); + } + + if (!commandLibrary.isReservedCommandKeyword(aliasValue)) { + return new CommandResult(String.format( + MESSAGE_FAILURE_NON_ORIGINAL_COMMAND, aliasValue)); + } + + commandLibrary.addNewAlias(aliasKey, aliasValue); + + return new CommandResult(String.format(MESSAGE_SUCCESS, aliasKey, aliasValue)); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} +``` +###### /java/seedu/agendum/logic/commands/CommandLibrary.java +``` java + /** + * Replace the current commandLibrary's aliasTable with the new aliasTable provided + */ + public void loadAliasTable(Hashtable aliasTable) { + this.aliasTable = aliasTable; + } + + /** + * Returns true if key is already an alias for a command keyword, false otherwise. + */ + public boolean isExistingAliasKey(String key) { + assert key != null; + assert key.equals(key.toLowerCase()); + + return aliasTable.containsKey(key); + } + + /** + * Returns the reserved command keyword that is aliased by key + * + * @param key An existing user-defined alias for a reserved command keyword + * @return The associated reserved command keyword + */ + public String getAliasedValue(String key) { + assert isExistingAliasKey(key); + + return aliasTable.get(key); + } + + /** + * Returns true if value is a reserved command keyword, false otherwise + */ + public boolean isReservedCommandKeyword(String value) { + assert value != null; + assert value.equals(value.toLowerCase()); + + return allCommandWords.contains(value); + } + + /** + * Pre-condition: key is a new unique alias and not a command keyword; + * value is a reserved command keyword. + * Saves the new alias relationship between key and value. + * + * @param key A valid and unique user-defined alias for a reserved command word + * @param value The target reserved command word + */ + public void addNewAlias(String key, String value) { + assert !isExistingAliasKey(key); + assert !isReservedCommandKeyword(key); + assert isReservedCommandKeyword(value); + + aliasTable.put(key, value); + + indicateAliasAdded(key, value); + } + + /** + * Destroy the alias relationship (key can no longer be used in place of command word) + * + * @param key An existing user-defined alias for a reserved command word + */ + public void removeExistingAlias(String key) { + assert isExistingAliasKey(key); + + String value = aliasTable.remove(key); + + indicateAliasRemoved(key, value); + } + + /** + * Raises an event to indicate that an alias has been added to aliasTable in the command library + * + * @param key The new user-defined alias key + * @param value The target reserved command word + */ + private void indicateAliasAdded(String key, String value) { + String message = "Added alias " + key + " for " + value; + EventsCenter eventCenter = EventsCenter.getInstance(); + eventCenter.post(new AliasTableChangedEvent(message, aliasTable)); + } + + /** + * Raises an event to indicate that an alias has been removed from aliasTable in the command library + * + * @param key The alias key to be removed + * @param value The associated reserved command word + */ + private void indicateAliasRemoved(String key, String value) { + String message = "Removed alias " + key + " for " + value; + EventsCenter eventCenter = EventsCenter.getInstance(); + eventCenter.post(new AliasTableChangedEvent(message, aliasTable)); + } + +} +``` +###### /java/seedu/agendum/logic/commands/CommandResult.java +``` java + /** + * Pre-condition: tasks and originalIndices must be of the same size. + * Returns a string containing each task in tasks + * with the corresponding number in originalIndices prepended + * + * @param tasks List of tasks where each task is be prepended by an index + * @param originalIndices List of corresponding index for each task + * @return String containing all tasks labeled with their corresponding index + */ + public static String tasksToString(List tasks, List originalIndices) { + final StringBuilder builder = new StringBuilder(); + builder.append("\n"); + for (int i = 0; i < tasks.size(); i++) { + builder.append("#").append(originalIndices.get(i)).append(": "); + builder.append(tasks.get(i).getAsText()); + } + return builder.toString(); + } + +} +``` +###### /java/seedu/agendum/logic/commands/DeleteCommand.java +``` java +/** + * Deletes task(s) identified using their last displayed indices from the task listing. + */ +public class DeleteCommand extends Command { + + public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_FORMAT = "delete "; + public static final String COMMAND_DESCRIPTION = "delete task(s) from Agendum"; + public static final String MESSAGE_DELETE_TASK_SUCCESS = "Deleted Task(s): %1$s"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "(The id must be a positive number)\n" + + "Example: " + COMMAND_WORD + " 7 10-11"; + + private ArrayList targetIndexes; + private ArrayList tasksToDelete; + + + public DeleteCommand(Set targetIndexes) { + this.targetIndexes = new ArrayList(targetIndexes); + Collections.sort(this.targetIndexes); + this.tasksToDelete = new ArrayList(); + } + + @Override + public CommandResult execute() { + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (isAnyIndexInvalid(lastShownList)) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + for (int targetIndex: targetIndexes) { + ReadOnlyTask taskToDelete = lastShownList.get(targetIndex - 1); + tasksToDelete.add(taskToDelete); + } + + try { + model.deleteTasks(tasksToDelete); + } catch (TaskNotFoundException pnfe) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } + + return new CommandResult(String.format(MESSAGE_DELETE_TASK_SUCCESS, + CommandResult.tasksToString(tasksToDelete, targetIndexes))); + } + + private boolean isAnyIndexInvalid(UnmodifiableObservableList lastShownList) { + return targetIndexes.stream().anyMatch(index -> index > lastShownList.size()); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} +``` +###### /java/seedu/agendum/logic/commands/MarkCommand.java +``` java +/** + * Mark task(s) identified using their last displayed indices in the task listing. + */ +public class MarkCommand extends Command { + + public static final String COMMAND_WORD = "mark"; + public static final String COMMAND_FORMAT = "mark "; + public static final String COMMAND_DESCRIPTION = "mark task(s) as completed"; + + public static final String MESSAGE_MARK_TASK_SUCCESS = "Marked Task(s)!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "(The id must be a positive number)\n" + + "Example: " + COMMAND_WORD + " 1 3 5-6"; + + private ArrayList targetIndexes; + private ArrayList tasksToMark; + + + public MarkCommand(Set targetIndexes) { + this.targetIndexes = new ArrayList(targetIndexes); + Collections.sort(this.targetIndexes); + this.tasksToMark = new ArrayList(); + } + + @Override + public CommandResult execute() { + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (isAnyIndexInvalid(lastShownList)) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + for (int targetIndex: targetIndexes) { + ReadOnlyTask taskToMark = lastShownList.get(targetIndex - 1); + tasksToMark.add(taskToMark); + } + + try { + model.markTasks(tasksToMark); + } catch (TaskNotFoundException pnfe) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } catch (DuplicateTaskException pnfe) { + model.resetDataToLastSavedList(); + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } + + return new CommandResult(MESSAGE_MARK_TASK_SUCCESS); + } + + private boolean isAnyIndexInvalid(UnmodifiableObservableList lastShownList) { + return targetIndexes.stream().anyMatch(index -> index > lastShownList.size()); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} +``` +###### /java/seedu/agendum/logic/commands/RenameCommand.java +``` java +/** + * Renames the target task in the task listing. + */ +public class RenameCommand extends Command { + + public static final String COMMAND_WORD = "rename"; + public static final String COMMAND_FORMAT = "rename "; + public static final String COMMAND_DESCRIPTION = "update the name of a task"; + public static final String MESSAGE_SUCCESS = "Task renamed: %1$s"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " 2 Watch Star Trek"; + + private int targetIndex; + private Name newTaskName; + + /** + * Constructor for rename command + * @throws IllegalValueException if the name is invalid + */ + public RenameCommand(int targetIndex, String name) throws IllegalValueException { + this.targetIndex = targetIndex; + this.newTaskName = new Name(name); + } + + @Override + public CommandResult execute() { + assert model != null; + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + ReadOnlyTask taskToRename = lastShownList.get(targetIndex - 1); + + try { + Task renamedTask = new Task(taskToRename); + renamedTask.setName(newTaskName); + model.updateTask(taskToRename, renamedTask); + } catch (UniqueTaskList.DuplicateTaskException e) { + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } catch (TaskNotFoundException e) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, newTaskName)); + + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} +``` +###### /java/seedu/agendum/logic/commands/UnaliasCommand.java +``` java +/** + * Create an alias for a reserved command keyword + */ +public class UnaliasCommand extends Command { + + public static final String COMMAND_WORD = "unalias"; + public static final String COMMAND_FORMAT = "unalias "; + public static final String COMMAND_DESCRIPTION = "remove a shorthand command"; + + public static final String MESSAGE_SUCCESS = "Removed alias <%1$s>"; + public static final String MESSAGE_FAILURE_NO_ALIAS_KEY = "The alias <%1$s> does not exist"; + public static final String MESSAGE_FAILURE_RESERVED_COMMAND_WORD = "<%1$s> is a reserved command word"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " m\n" + + "(if m is aliased to mark)"; + + private String aliasKey; + private CommandLibrary commandLibrary; + + public UnaliasCommand(String aliasKey) { + this.aliasKey = aliasKey; + } + + public void setData(Model model, CommandLibrary commandLibrary) { + this.model = model; + this.commandLibrary = commandLibrary; + } + + @Override + public CommandResult execute() { + if (commandLibrary.isReservedCommandKeyword(aliasKey)) { + return new CommandResult(String.format(MESSAGE_FAILURE_RESERVED_COMMAND_WORD, aliasKey)); + } + + if (!commandLibrary.isExistingAliasKey(aliasKey)) { + return new CommandResult(String.format(MESSAGE_FAILURE_NO_ALIAS_KEY, aliasKey)); + } + + commandLibrary.removeExistingAlias(aliasKey); + return new CommandResult(String.format(MESSAGE_SUCCESS, aliasKey)); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} +``` +###### /java/seedu/agendum/logic/commands/UndoCommand.java +``` java +/** + * Undo the last change to the to-do list + */ +public class UndoCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + public static final String COMMAND_FORMAT = "undo"; + public static final String COMMAND_DESCRIPTION = "undo the last change to your to-do list"; + + public static final String MESSAGE_SUCCESS = "Previous change undone!"; + public static final String MESSAGE_FAILURE = "Nothing to undo!"; + + @Override + public CommandResult execute() { + assert model != null; + + try { + model.restorePreviousToDoList(); + } catch (NoPreviousListFoundException nplfe) { + return new CommandResult(MESSAGE_FAILURE); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} +``` +###### /java/seedu/agendum/logic/commands/UnmarkCommand.java +``` java +/** + * Unmark task(s) identified using their last displayed indices in the task listing. + */ +public class UnmarkCommand extends Command { + + public static final String COMMAND_WORD = "unmark"; + public static final String COMMAND_FORMAT = "unmark "; + public static final String COMMAND_DESCRIPTION = "unmark task(s) from completed"; + + public static final String MESSAGE_UNMARK_TASK_SUCCESS = "Unmarked Task(s)!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "(The id must be a positive number)\n" + + "Example: " + COMMAND_WORD + " 11-13 15"; + + private ArrayList targetIndexes; + private ArrayList tasksToUnmark; + +``` +###### /java/seedu/agendum/logic/commands/UnmarkCommand.java +``` java + public UnmarkCommand(Set targetIndexes) { + this.targetIndexes = new ArrayList<>(targetIndexes); + Collections.sort(this.targetIndexes); + this.tasksToUnmark = new ArrayList<>(); + } + + @Override + public CommandResult execute() { + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (isAnyIndexInvalid(lastShownList)) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + for (int targetIndex: targetIndexes) { + ReadOnlyTask taskToUnmark = lastShownList.get(targetIndex - 1); + tasksToUnmark.add(taskToUnmark); + } + + try { + model.unmarkTasks(tasksToUnmark); + } catch (TaskNotFoundException pnfe) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } catch (DuplicateTaskException pnfe) { + model.resetDataToLastSavedList(); + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } + + return new CommandResult(MESSAGE_UNMARK_TASK_SUCCESS); + } + + private boolean isAnyIndexInvalid(UnmodifiableObservableList lastShownList) { + return targetIndexes.stream().anyMatch(index -> index > lastShownList.size()); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} +``` +###### /java/seedu/agendum/logic/parser/Parser.java +``` java + /** + * Parses arguments in the context of the delete task(s) command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareDelete(String args) { + Set taskIds = parseIndexes(args); + if (taskIds.isEmpty()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + return new DeleteCommand(taskIds); + } + + /** + * Parses arguments in the context of the mark task(s) command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareMark(String args) { + Set taskIds = parseIndexes(args); + if (taskIds.isEmpty()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkCommand.MESSAGE_USAGE)); + } + + return new MarkCommand(taskIds); + } + + /** + * Parses arguments in the context of the unmark task(s) command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareUnmark(String args) { + Set taskIds = parseIndexes(args); + if (taskIds.isEmpty()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnmarkCommand.MESSAGE_USAGE)); + } + + return new UnmarkCommand(taskIds); + } + + /** + * Parses arguments in the context of the rename task command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareRename(String args) { + final Matcher matcher = RENAME_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RenameCommand.MESSAGE_USAGE)); + } + + final String givenName = matcher.group("name").trim(); + final String givenIndex = matcher.group("targetIndex"); + Optional index = parseIndex(givenIndex); + + if (!index.isPresent()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RenameCommand.MESSAGE_USAGE)); + } + + try { + return new RenameCommand(index.get(), givenName); + } catch (IllegalValueException ive) { + return new IncorrectCommand(ive.getMessage()); + } + } + + /** + * Parses arguments in the context of the alias command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareAlias(String args) { + final Matcher matcher = ALIAS_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AliasCommand.MESSAGE_USAGE)); + } + + String aliasKey = matcher.group("shorthand").toLowerCase(); + String aliasValue = matcher.group("commandword").toLowerCase(); + + return new AliasCommand(aliasKey, aliasValue); + } + + /** + * Parses arguments in the context of the unalias command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareUnalias(String args) { + final Matcher matcher = UNALIAS_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnaliasCommand.MESSAGE_USAGE)); + } + + String aliasKey = matcher.group("shorthand").toLowerCase(); + + return new UnaliasCommand(aliasKey); + } + +``` +###### /java/seedu/agendum/logic/parser/Parser.java +``` java + /** + * Returns the specified indices in the {@code command} if positive unsigned integer(s) are given. + * Returns an empty set otherwise. + */ + private Set parseIndexes(String args) { + final Matcher matcher = TASK_INDEXES_ARGS_FORMAT.matcher(args.trim()); + Set emptySet = new HashSet(); + Set taskIds = new HashSet(); + + if (!matcher.matches()) { + return emptySet; + } + + String replacedArgs = args.replaceAll("[ ]+", ",").replaceAll(",+", ","); + + String[] taskIdStrings = replacedArgs.split(","); + for (String taskIdString : taskIdStrings) { + if (taskIdString.matches("\\d+")) { + taskIds.add(Integer.parseInt(taskIdString)); + } else if (taskIdString.matches("\\d+-\\d+")) { + String[] startAndEndIndexes = taskIdString.split("-"); + int startIndex = Integer.parseInt(startAndEndIndexes[0]); + int endIndex = Integer.parseInt(startAndEndIndexes[1]); + taskIds.addAll(IntStream.rangeClosed(startIndex, endIndex) + .boxed().collect(Collectors.toList())); + } + } + + if (taskIds.remove(0)) { + return emptySet; + } + + return taskIds; + } + +``` +###### /java/seedu/agendum/model/ModelManager.java +``` java + /** + * Signals that an operation to remove a list from the stack of previous lists would fail + * as the stack must contain at least one list. + */ + public static class NoPreviousListFoundException extends Exception {} +``` +###### /java/seedu/agendum/model/ModelManager.java +``` java + @Override + public void resetData(ReadOnlyToDoList newData) { + mainToDoList.resetData(newData); + logger.fine("[MODEL] --- successfully reset data of the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } +``` +###### /java/seedu/agendum/model/ModelManager.java +``` java + @Override + public synchronized void deleteTasks(List targets) throws TaskNotFoundException { + for (ReadOnlyTask target: targets) { + mainToDoList.removeTask(target); + removeTaskFromSyncManager(target); + } + + logger.fine("[MODEL] --- successfully deleted all specified targets from the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } + + @Override + public synchronized void addTask(Task task) throws UniqueTaskList.DuplicateTaskException { + mainToDoList.addTask(task); + + logger.fine("[MODEL] --- successfully added the new task to the to-do list"); + backupCurrentToDoList(); + updateFilteredListToShowAll(); + indicateToDoListChanged(); + addTaskToSyncManager(task); + } + + @Override + public synchronized void updateTask(ReadOnlyTask target, Task updatedTask) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + mainToDoList.updateTask(target, updatedTask); + + logger.fine("[MODEL] --- successfully updated the target task in the to-do list"); + backupCurrentToDoList(); + updateFilteredListToShowAll(); + indicateToDoListChanged(); + + addTaskToSyncManager(updatedTask); + removeTaskFromSyncManager(target); + } + + @Override + public synchronized void markTasks(List targets) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + for (ReadOnlyTask target: targets) { + mainToDoList.markTask(target); + } + + logger.fine("[MODEL] --- successfully marked all specified targets from the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } + + @Override + public synchronized void unmarkTasks(List targets) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + for (ReadOnlyTask target: targets) { + mainToDoList.unmarkTask(target); + } + + logger.fine("[MODEL] --- successfully unmarked all specified targets from the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } + + /** + * Restores the previous (second latest) list saved in the stack of previous lists. + */ + @Override + public synchronized void restorePreviousToDoList() throws NoPreviousListFoundException { + removeLastSavedToDoList(); + resetDataToLastSavedList(); + logger.fine("[MODEL] --- successfully restored the previous the to-do list from this session"); + indicateToDoListChanged(); + } + + /** + * Resets the {@code mainToDoList} data to match the top list in the {@code previousLists} stack + */ + @Override + public void resetDataToLastSavedList() { + assert !previousLists.empty(); + ToDoList lastSavedListFromHistory = previousLists.peek(); + mainToDoList.resetData(lastSavedListFromHistory); + } + + private void backupCurrentToDoList() { + ToDoList latestList = new ToDoList(this.getToDoList()); + previousLists.push(latestList); + } + + private void clearAllPreviousToDoLists() { + previousLists.clear(); + } + + /** + * Pops the top list from the {@code previousLists} stack if there are more than 1 list present + * @throws NoPreviousListFoundException if there is only 1 list in the stack + */ + private void removeLastSavedToDoList() throws NoPreviousListFoundException { + assert !previousLists.empty(); + + if (previousLists.size() == 1) { + throw new NoPreviousListFoundException(); + } + + previousLists.pop(); + } + + +``` +###### /java/seedu/agendum/model/task/ReadOnlyTask.java +``` java + /** + * Format the tasks as text, showing all fine details including name, + * completion status, start and end time if any and last updated time + */ + default String getDetailedText() { + String completionStatus = (isCompleted()) ? "Completed" : "Incomplete"; + String startTime = (getStartDateTime().isPresent()) ? getStartDateTime().get().toString() + : "None"; + String endTime = (getEndDateTime().isPresent()) ? getEndDateTime().get().toString() + : "None"; + String lastUpdatedTime = getLastUpdatedTime().toString(); + + final StringBuilder builder = new StringBuilder(); + builder.append("Task name: ") + .append(getName()) + .append(" Completion Status: ") + .append(completionStatus) + .append(" Start Time: ") + .append(startTime) + .append(" End Time: ") + .append(endTime) + .append(" Last Updated Time: ") + .append(lastUpdatedTime); + return builder.toString(); + } + +} +``` +###### /java/seedu/agendum/model/task/Task.java +``` java +/** + * Represents a Task in the to do list. + */ +public class Task implements ReadOnlyTask, Comparable { + + private static final int UPCOMING_DAYS_THRESHOLD = 7; + + private Name name_; + private boolean isCompleted_; + private LocalDateTime startDateTime_; + private LocalDateTime endDateTime_; + private LocalDateTime lastUpdatedTime_; + + // ================ Constructor methods ============================== + + /** + * Constructor for a floating task (with no deadline/start time or end time) + */ + public Task(Name name) { + assert CollectionUtil.isNotNull(name); + this.name_ = name; + this.isCompleted_ = false; + this.startDateTime_ = null; + this.endDateTime_ = null; + setLastUpdatedTimeToNow(); + } + + /** + * Constructor for a task with deadline only + */ + public Task(Name name, Optional deadline) { + assert CollectionUtil.isNotNull(name); + this.name_ = name; + this.isCompleted_ = false; + this.startDateTime_ = null; + this.endDateTime_ = deadline.orElse(null); + this.setLastUpdatedTimeToNow(); + } + + /** + * Constructor for a task (event) with both a start and end time + */ + public Task(Name name, Optional startDateTime, + Optional endDateTime) { + assert CollectionUtil.isNotNull(name); + this.name_ = name; + this.isCompleted_ = false; + this.startDateTime_ = startDateTime.orElse(null); + this.endDateTime_ = endDateTime.orElse(null); + this.setLastUpdatedTimeToNow(); + } + + /** + * Copy constructor. + */ + public Task(ReadOnlyTask source) { + this(source.getName(), source.getStartDateTime(), source.getEndDateTime()); + if (source.isCompleted()) { + this.markAsCompleted(); + } + this.setLastUpdatedTime(source.getLastUpdatedTime()); + } + + // ================ Getter methods ============================== + + @Override + public Name getName() { + return name_; + } + + @Override + public boolean isCompleted() { + return isCompleted_; + } + + /** + * Returns true if a task is uncompleted and has a start/end time + * that is after the current time but within some threshold amount of days + */ + @Override + public boolean isUpcoming() { + if (isCompleted()) { + return false; + } + + if (!hasTime()) { + return false; + } + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime thresholdTime = currentTime.plusDays(UPCOMING_DAYS_THRESHOLD); + boolean isBeforeUpcomingDaysThreshold = getTaskTime().isBefore(thresholdTime); + boolean isAfterCurrentTime = getTaskTime().isAfter(currentTime); + + return isBeforeUpcomingDaysThreshold && isAfterCurrentTime; + } + + /** + * Returns true is a task is uncompleted and has a start/end time + * that is before the current time + */ + @Override + public boolean isOverdue() { + if (isCompleted()) { + return false; + } + + if (!hasTime()) { + return false; + } + + LocalDateTime currentTime = LocalDateTime.now(); + boolean isBeforeCurrentTime = getTaskTime().isBefore(currentTime); + + return isBeforeCurrentTime; + } + + /** + * Returns true if a task has a start time or an end time, false otherwise + * This must be called to check if comparison of task's time is possible + */ + @Override + public boolean hasTime() { + return getStartDateTime().isPresent() || getEndDateTime().isPresent(); + } + + /** + * Returns true if the task has a start time and end time, false otherwise. + */ + @Override + public boolean isEvent() { + return getStartDateTime().isPresent() && getEndDateTime().isPresent(); + } + + /** + * Returns true if the task has a deadline (i.e. only a end time), false otherwise. + */ + @Override + public boolean hasDeadline() { + return !getStartDateTime().isPresent() && getEndDateTime().isPresent(); + } + + @Override + public Optional getStartDateTime() { + return Optional.ofNullable(startDateTime_); + } + + @Override + public Optional getEndDateTime() { + return Optional.ofNullable(endDateTime_); + } + + /** + * Returns the time the task is last updated. + * e.g. created, renamed, rescheduled, marked or unmarked + */ + @Override + public LocalDateTime getLastUpdatedTime() { + return lastUpdatedTime_; + } + + /** + * Pre-condition: Task has a start or end time. + * Returns the start time if present, else returns the end time. + */ + private LocalDateTime getTaskTime() { + assert hasTime(); + return getStartDateTime().orElse(getEndDateTime().get()); + } + + // ================ Setter methods ============================== + + public void setName(Name name) { + this.name_ = name; + setLastUpdatedTimeToNow(); + } + + public void markAsCompleted() { + this.isCompleted_ = true; + setLastUpdatedTimeToNow(); + } + + public void markAsUncompleted() { + this.isCompleted_ = false; + setLastUpdatedTimeToNow(); + } + + public void setStartDateTime(Optional startDateTime) { + this.startDateTime_ = startDateTime.orElse(null); + setLastUpdatedTimeToNow(); + } + + public void setEndDateTime(Optional endDateTime) { + this.endDateTime_ = endDateTime.orElse(null); + setLastUpdatedTimeToNow(); + } + + public void setLastUpdatedTime(LocalDateTime updatedTime) { + this.lastUpdatedTime_ = updatedTime; + } + + public void setLastUpdatedTimeToNow() { + // nano-seconds is set to 0 for more consistent test results when (un)marking multiple tasks + this.lastUpdatedTime_ = LocalDateTime.now().withNano(0); + } + + // ================ Other methods ============================== + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ReadOnlyTask // instanceof handles nulls + && this.isSameStateAs((ReadOnlyTask) other)); + } + + /** + * Compares the current task with another Task other. + * The current task is considered to be less than the other task if + * 1) it is uncompleted and other is completed + * 2) both tasks are completed but this task has a earlier start/end time associated + * 3) both tasks are uncompleted but this task has a later updated time + * 4) both tasks are uncompleted with the same updated time + * but this task has a lexicographically smaller name (useful when sorting tasks in testing) + */ + @Override + public int compareTo(Task other) { + int comparedCompletionStatus = compareCompletionStatus(other); + if (comparedCompletionStatus != 0) { + return comparedCompletionStatus; + } + + int comparedTaskTime = compareTaskTime(other); + if (!isCompleted() && comparedTaskTime != 0) { + return comparedTaskTime; + } + + int comparedLastUpdatedTime = compareLastUpdatedTime(other); + if (comparedLastUpdatedTime != 0) { + return comparedLastUpdatedTime; + } + + return compareName(other); + } + + /** + * Compares the completion status of current task with another Task other. + * The current task is considered to be less than the other task if + * it is uncompleted and other is completed + */ + public int compareCompletionStatus(Task other) { + return Boolean.compare(this.isCompleted(), other.isCompleted()); + } + + /** + * Compares the earliest time of the current task with another Task other. + * The current task is considered to be less than the other task if + * 1) both tasks have a time associated but this task has a earlier time associated + * 2) this task has a time associated but the other task does not. + * Both tasks are equal if they have no time or the same earliest time associated. + * Time refers to value returned by {@link #getTaskTime()} + */ + public int compareTaskTime(Task other) { + if (this.hasTime() && other.hasTime()) { + return this.getTaskTime().compareTo(other.getTaskTime()); + } else if (this.hasTime()) { + return -1; + } else if (other.hasTime()) { + return 1; + } else { + return 0; + } + } + + /** + * Compares the current task with another Task other. + * The current task is considered to be less than the other task if + * it has a later updated time + */ + public int compareLastUpdatedTime(Task other) { + return other.getLastUpdatedTime().compareTo(this.getLastUpdatedTime()); + } + + /** + * Compares the current task with another Task other. + * The current task is considered to be less than the other task if + * it has a lexicographically smaller name + */ + public int compareName(Task other) { + return this.getName().toString().compareTo(other.getName().toString()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name_, isCompleted_, startDateTime_, endDateTime_); + } + + public int syncCode() { + return hashCode(); + } + + @Override + public String toString() { + return getAsText(); + } + +} +``` +###### /java/seedu/agendum/model/task/UniqueTaskList.java +``` java + /** + * Adds a task to the list. + * + * @throws DuplicateTaskException if the task to add is a duplicate of an existing task in the list. + */ + public void add(Task toAdd) throws DuplicateTaskException { + assert toAdd != null; + + if (contains(toAdd)) { + logger.fine("[TASK LIST] --- Duplicate Task: " + toAdd.getDetailedText()); + EventsCenter.getInstance().post(new JumpToListRequestEvent(toAdd, false)); + throw new DuplicateTaskException(); + } + + internalList.add(toAdd); + EventsCenter.getInstance().post(new JumpToListRequestEvent(toAdd, false)); + + logger.fine("[TASK LIST] --- Added a Task: " + toAdd.getDetailedText()); + } + + /** + * Removes the equivalent task from the list. + * + * @throws TaskNotFoundException if no such task could be found in the list. + */ + public boolean remove(ReadOnlyTask toRemove) throws TaskNotFoundException { + assert toRemove != null; + final boolean taskFoundAndDeleted = internalList.remove(toRemove); + + if (!taskFoundAndDeleted) { + logger.fine("[TASK LIST] --- Missing Task: " + toRemove.getDetailedText()); + throw new TaskNotFoundException(); + } + + logger.fine("[TASK LIST] --- Deleted a Task: " + toRemove.getDetailedText()); + + return taskFoundAndDeleted; + } + + /** + * Replaces the equivalent task (to toUpdate) in the list with a new task (updatedTask). + * + * @throws TaskNotFoundException if no such task (toUpdate) could be found in the list. + * @throws DuplicateTaskException if the updated task is a duplicate of an existing task in the list. + */ + public boolean update(ReadOnlyTask toUpdate, Task updatedTask) + throws TaskNotFoundException, DuplicateTaskException { + assert toUpdate != null; + assert updatedTask != null; + + final int taskIndex = internalList.indexOf(toUpdate); + final boolean taskFoundAndUpdated = (taskIndex != -1); + + if (!taskFoundAndUpdated) { + logger.fine("[TASK LIST] --- Missing Task: " + toUpdate.getDetailedText()); + throw new TaskNotFoundException(); + } + + if (contains(updatedTask)) { + logger.fine("[TASK LIST] --- Duplicate Task: " + toUpdate.getDetailedText()); + EventsCenter.getInstance().post(new JumpToListRequestEvent(updatedTask, true)); + throw new DuplicateTaskException(); + } + + internalList.set(taskIndex, updatedTask); + EventsCenter.getInstance().post(new JumpToListRequestEvent(updatedTask, true)); + logger.fine("[TASK LIST] --- Updated Task: " + toUpdate.getDetailedText() + + " updated to " + updatedTask.getDetailedText()); + + return taskFoundAndUpdated; + } + + /** + * Marks the equivalent task in the list. + * + * @throws TaskNotFoundException if no such task could be found in the list. + * @throws DuplicateTaskException if a duplicate will result from marking the task + */ + public boolean mark(ReadOnlyTask toMark) throws TaskNotFoundException, DuplicateTaskException { + assert toMark != null; + + logger.fine("[TASK LIST] --- Attempt to Mark Task: " + toMark.getDetailedText()); + + Task markedTask = new Task(toMark); + markedTask.markAsCompleted(); + boolean taskFoundAndMarked = update(toMark, markedTask); + + return taskFoundAndMarked; + } + + /** + * Unmarks the equivalent task in the list. + * + * @throws TaskNotFoundException if no such task could be found in the list. + * @throws DuplicateTaskException if a duplicate will result from unmarking the task + */ + public boolean unmark(ReadOnlyTask toUnmark) throws TaskNotFoundException, DuplicateTaskException { + assert toUnmark != null; + + logger.fine("[TASK LIST] --- Attempt to Unmark Task: " + toUnmark.getDetailedText()); + + Task unmarkedTask = new Task(toUnmark); + unmarkedTask.markAsUncompleted(); + boolean taskFoundAndUnmarked = update(toUnmark, unmarkedTask); + + return taskFoundAndUnmarked; + } + +``` +###### /java/seedu/agendum/model/ToDoList.java +``` java + /** + * Updates an existing task in the to-do list. + * + * @throws DuplicateTaskException if an equivalent task (to updatedTask) already exists. + * @throws TaskNotFoundException if no such task (key) could be found in the list. + */ + public boolean updateTask(ReadOnlyTask key, Task updatedTask) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + return tasks.update(key, updatedTask); + } + + /** + * Marks an existing task in the to-do list. + * + * @throws DuplicateTaskException if a duplicate task would result after marking key. + * @throws TaskNotFoundException if no such task (key) could be found in the list. + */ + public boolean markTask(ReadOnlyTask key) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + return tasks.mark(key); + } + + /** + * Unmarks an existing task in the to-do list. + * + * @throws DuplicateTaskException if a duplicate task would result after unmarking key. + * @throws TaskNotFoundException if no such task (key) could be found in the list. + */ + public boolean unmarkTask(ReadOnlyTask key) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + return tasks.unmark(key); + } +``` diff --git a/collated/main/A0148031R.md b/collated/main/A0148031R.md new file mode 100644 index 000000000000..a13e0b2509a1 --- /dev/null +++ b/collated/main/A0148031R.md @@ -0,0 +1,1858 @@ +# A0148031R +###### /java/seedu/agendum/commons/events/ui/CloseHelpWindowRequestEvent.java +``` java +/** + * An event that requests to close help window + */ +public class CloseHelpWindowRequestEvent extends BaseEvent{ + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} +``` +###### /java/seedu/agendum/commons/events/ui/JumpToListRequestEvent.java +``` java +/** + * Indicates a request to jump to the list of tasks + */ +public class JumpToListRequestEvent extends BaseEvent { + + public final Task targetTask; + public final boolean hasMultipleTasks; + + public JumpToListRequestEvent(Task task, boolean hasMultipleTasks) { + this.targetTask = task; + this.hasMultipleTasks = hasMultipleTasks; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} +``` +###### /java/seedu/agendum/commons/events/ui/ShowHelpRequestEvent.java +``` java +/** + * An event requesting to view the help page. + */ +public class ShowHelpRequestEvent extends BaseEvent { + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} +``` +###### /java/seedu/agendum/logic/commands/HelpCommand.java +``` java +/** + * Format full help instructions for every command for display. + */ +public class HelpCommand extends Command { + + // COMMAND_WORD, COMMAND_FORMAT, COMMAND_DESCRIPTION are for display in help window + public static final String COMMAND_WORD = "help"; + public static final String COMMAND_FORMAT = "help"; + public static final String COMMAND_DESCRIPTION = "view a summary of Agendum commands"; + public static final String MESSAGE_USAGE = COMMAND_WORD + "- " + + COMMAND_DESCRIPTION; + public static final String SHOWING_HELP_MESSAGE = "Opened help window."; + + @Override + public CommandResult execute() { + EventsCenter.getInstance().post(new ShowHelpRequestEvent()); + return new CommandResult(SHOWING_HELP_MESSAGE); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} +``` +###### /java/seedu/agendum/model/UserPrefs.java +``` java + /** + * Sets default window to be screen size + */ + public UserPrefs(){ + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + this.setGuiSettings(screenSize.getWidth(), screenSize.getHeight(), 0, 0); + } + + public void setGuiSettings(double width, double height, int x, int y) { + guiSettings = new GuiSettings(width, height, x, y); + } + +``` +###### /java/seedu/agendum/ui/CommandBox.java +``` java +/** + * Controller for the command box field + * + */ +public class CommandBox extends UiPart { + private final Logger logger = LogsCenter.getLogger(CommandBox.class); + private static final String FXML = "CommandBox.fxml"; + private static final String FIND_COMMAND = "find "; + private static final String HELP_COMMAND = "help"; + private static final String RESULT_FEEDBACK = "Result: "; + private static final String ERROR = "error"; + private static final Color MESSAGE_COLOR = Color.web("#ffffff"); + + private AnchorPane placeHolderPane; + private AnchorPane commandPane; + private StackPane messagePlaceHolder; + private ResultPopUp resultPopUp; + private static CommandBoxHistory commandBoxHistory; + + private Logic logic; + + @FXML + private TextField commandTextField; + private CommandResult mostRecentResult; + + public static CommandBox load(Stage primaryStage, AnchorPane commandBoxPlaceholder, StackPane messagePlaceHolder, + ResultPopUp resultPopUp, Logic logic) { + CommandBox commandBox = UiPartLoader.loadUiPart(primaryStage, commandBoxPlaceholder, new CommandBox()); + commandBox.configure(resultPopUp, messagePlaceHolder, logic); + commandBox.addToPlaceholder(); + commandBoxHistory = CommandBoxHistory.getInstance(); + return commandBox; + } + + public void configure(ResultPopUp resultPopUp, StackPane messagePlaceHolder, Logic logic) { + this.resultPopUp = resultPopUp; + this.messagePlaceHolder = messagePlaceHolder; + this.logic = logic; + registerAsAnEventHandler(this); + registerArrowKeyEventFilter(); + registerTabKeyEventFilter(); + } + + private void addToPlaceholder() { + SplitPane.setResizableWithParent(placeHolderPane, false); + FxViewUtil.applyAnchorBoundaryParameters(commandPane, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(commandTextField, 0.0, 0.0, 0.0, 0.0); + placeHolderPane.getChildren().add(commandTextField); + } + + @Override + public void setNode(Node node) { + commandPane = (AnchorPane) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + public void setPlaceholder(AnchorPane pane) { + this.placeHolderPane = pane; + } + + /** + * Executes the command and saves this command to history if comamnd input + * is changed + */ + @FXML + private void handleCommandInputChanged() { + //Take a copy of the command text + commandBoxHistory.saveNewCommand(commandTextField.getText()); + String previousCommandTest = commandBoxHistory.getLastCommand(); + if(previousCommandTest.toLowerCase().trim().startsWith(FIND_COMMAND) && + previousCommandTest.toLowerCase().trim().length() > FIND_COMMAND.length()) { + postMessage(Messages.MESSAGE_ESCAPE_HELP_WINDOW); + } else { + raise(new CloseHelpWindowRequestEvent()); + } + + /* We assume the command is correct. If it is incorrect, the command box will be changed accordingly + * in the event handling code {@link #handleIncorrectCommandAttempted} + */ + + setStyleToIndicateCorrectCommand(); + mostRecentResult = logic.execute(previousCommandTest); + if(!previousCommandTest.toLowerCase().equals(HELP_COMMAND)) { + resultPopUp.postMessage(mostRecentResult.feedbackToUser); + } + logger.info(RESULT_FEEDBACK + mostRecentResult.feedbackToUser); + } + + /** + * Post meesage in the message place holder under the command box + */ + private void postMessage(String message) { + this.messagePlaceHolder.getChildren().clear(); + raise(new CloseHelpWindowRequestEvent()); + + Label label = new Label(message); + label.setTextFill(MESSAGE_COLOR); + label.setContentDisplay(ContentDisplay.CENTER); + label.setPadding(new Insets(0, 10, 0, 10)); + this.messagePlaceHolder.setAlignment(Pos.CENTER_LEFT); + this.messagePlaceHolder.getChildren().add(label); + } + + /** + * Sets arrow key for scrolling through command history + */ + private void registerArrowKeyEventFilter() { + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + KeyCode keyCode = event.getCode(); + if (keyCode.equals(KeyCode.UP)) { + String previousCommand = commandBoxHistory.getPreviousCommand(); + commandTextField.setText(previousCommand); + } else if (keyCode.equals(KeyCode.DOWN)) { + String nextCommand = commandBoxHistory.getNextCommand(); + commandTextField.setText(nextCommand); + } else { + return; + } + commandTextField.end(); + event.consume(); + }); + } + + /** + * Sets tab key for autocomplete + */ + private void registerTabKeyEventFilter() { + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + KeyCode keyCode = event.getCode(); + if (keyCode.equals(KeyCode.TAB)) { + Optional parsedString = EditDistanceCalculator.findCommandCompletion(commandTextField.getText()); + if(parsedString.isPresent()) { + commandTextField.setText(parsedString.get()); + } + } else { + return; + } + commandTextField.end(); + event.consume(); + }); + } + + /** + * Sets the command box style to indicate a correct command. + */ + private void setStyleToIndicateCorrectCommand() { + commandTextField.getStyleClass().remove("error"); + commandTextField.setText(""); + } + + @Subscribe + private void handleIncorrectCommandAttempted(IncorrectCommandAttemptedEvent event){ + logger.info(LogsCenter.getEventHandlingLogMessage( + event, "Invalid command: " + commandBoxHistory.getLastCommand())); + setStyleToIndicateIncorrectCommand(); + restoreCommandText(); + } + + /** + * Restores the command box text to the previously entered command + */ + private void restoreCommandText() { + commandTextField.setText(commandBoxHistory.getLastCommand()); + commandTextField.selectEnd(); + } + + /** + * Sets the command box style to indicate an error + */ + private void setStyleToIndicateIncorrectCommand() { + commandTextField.getStyleClass().add(ERROR); + } + +} +``` +###### /java/seedu/agendum/ui/CommandBoxHistory.java +``` java +/** + * Stores previous valid and invalid commands in a linked list with a max size + * New commands are added to the head of the linked list + */ +public class CommandBoxHistory { + + private static final int MAX_PREVIOUS_LINES = 15; + private static final String PREVIOUS_QUERY = "previous"; + private static final String NEXT_QUERY = "next"; + private static final String EMPTY_QUERY = ""; + private static final String EMPTY_COMMAND = ""; + private final LinkedList pastCommands; + private ListIterator iterator; + private String lastCommand = ""; + private String lastQuery = EMPTY_QUERY; + + private static CommandBoxHistory instance = null; + + private CommandBoxHistory() { + pastCommands = new LinkedList<>(); + iterator = pastCommands.listIterator(); + } + + public static CommandBoxHistory getInstance() { + if(instance == null) { + instance = new CommandBoxHistory(); + } + return instance; + } + + public String getLastCommand() { + return lastCommand; + } + + /** + * Retrieves the previous valid/invalid command. + * If there is no previous command, returns an empty string to clear the command box + */ + public String getPreviousCommand() { + if(!iterator.hasNext()) { + lastQuery = EMPTY_QUERY; + return EMPTY_COMMAND; + } else if(lastQuery.equals(NEXT_QUERY)) { + iterator.next(); + } + lastQuery = PREVIOUS_QUERY; + return iterator.next(); + } + + /** + * Retrieves the next valid/invalid command. + * If there is no next command, return an empty string to clear the command box + */ + public String getNextCommand() { + if (!iterator.hasPrevious()) { + lastQuery = EMPTY_QUERY; + return EMPTY_COMMAND; + } else if(lastQuery.equals(PREVIOUS_QUERY)) { + iterator.previous(); + } + lastQuery = NEXT_QUERY; + return iterator.previous(); + + } + + /** + * Takes in the latest command string entered and add it to command box history. + * Updates the iterator to point to the latest element + */ + public void saveNewCommand(String newCommand) { + lastCommand = newCommand; + pastCommands.addFirst(lastCommand); + + if (pastCommands.size() > MAX_PREVIOUS_LINES) { + pastCommands.removeLast(); + } + + iterator = pastCommands.listIterator(); + } + +} +``` +###### /java/seedu/agendum/ui/CompletedTasksPanel.java +``` java +/** + * Panel contains the list of completed tasks + */ +public class CompletedTasksPanel extends TasksPanel { + private static final String FXML = "CompletedTasksPanel.fxml"; + private static ObservableList mainTaskList; + private MultipleSelectionModel selectionModel; + + @FXML + private ListView completedTasksListView; + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + protected void setConnections(ObservableList taskList) { + mainTaskList = taskList; + completedTasksListView.setItems(taskList.filtered(ReadOnlyTask::isCompleted)); + completedTasksListView.setCellFactory(listView -> new CompletedTasksListViewCell()); + configure(); + } + + private void configure() { + selectionModel = completedTasksListView.getSelectionModel(); + completedTasksListView.setSelectionModel(null); + completedTasksListView.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume); + } + + /** + * Scrolls to the newly updated task and highlight for several seconds. If + * there are multiple tasks updated, previous highlight will not be cleared. + */ + public void scrollTo(Task task, boolean hasMultipleTasks) { + Platform.runLater(() -> { + + int index = mainTaskList.indexOf(task) - mainTaskList.filtered(t -> !t.isCompleted()).size(); + completedTasksListView.scrollTo(index); + completedTasksListView.setSelectionModel(selectionModel); + + if(hasMultipleTasks) { + completedTasksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + completedTasksListView.getSelectionModel().select(index); + } else { + completedTasksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + completedTasksListView.getSelectionModel().clearAndSelect(index); + } + + PauseTransition delay = new PauseTransition(Duration.seconds(5)); + delay.setOnFinished(event -> completedTasksListView.getSelectionModel().clearSelection(index)); + delay.play(); + }); + } + + class CompletedTasksListViewCell extends ListCell { + public CompletedTasksListViewCell() { + prefWidthProperty().bind(completedTasksListView.widthProperty()); + setMaxWidth(Control.USE_PREF_SIZE); + } + + @Override + protected void updateItem(ReadOnlyTask task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(TaskCard.load(task, mainTaskList.indexOf(task) + 1).getLayout()); + } + } + } +} +``` +###### /java/seedu/agendum/ui/FloatingTasksPanel.java +``` java +/** + * Panel contains the list of uncompleted floating tasks + */ +public class FloatingTasksPanel extends TasksPanel { + private static final String FXML = "FloatingTasksPanel.fxml"; + private static ObservableList mainTaskList; + private MultipleSelectionModel selectionModel; + + @FXML + private ListView floatingTasksListView; + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + protected void setConnections(ObservableList taskList) { + mainTaskList = taskList; + floatingTasksListView.setItems(taskList.filtered(task -> !task.isCompleted() && !task.hasTime())); + floatingTasksListView.setCellFactory(listView -> new FloatingTasksListViewCell()); + configure(); + } + + private void configure() { + selectionModel = floatingTasksListView.getSelectionModel(); + floatingTasksListView.setSelectionModel(null); + floatingTasksListView.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume); + } + + /** + * Scrolls to the newly updated task and highlight for several seconds. If + * there are multiple tasks updated, previous highlight will not be cleared. + */ + public void scrollTo(Task task, boolean hasMultipleTasks) { + Platform.runLater(() -> { + + int index = mainTaskList.indexOf(task) - + mainTaskList.filtered(t -> (t.hasTime() && !t.isCompleted())).size(); + floatingTasksListView.scrollTo(index); + floatingTasksListView.setSelectionModel(selectionModel); + + if(hasMultipleTasks) { + floatingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + floatingTasksListView.getSelectionModel().select(index); + } else { + floatingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + floatingTasksListView.getSelectionModel().clearAndSelect(index); + } + + PauseTransition delay = new PauseTransition(Duration.seconds(4)); + delay.setOnFinished(event -> floatingTasksListView.getSelectionModel().clearSelection(index)); + delay.play(); + }); + } + + class FloatingTasksListViewCell extends ListCell { + public FloatingTasksListViewCell() { + prefWidthProperty().bind(floatingTasksListView.widthProperty()); + setMaxWidth(Control.USE_PREF_SIZE); + } + + @Override + protected void updateItem(ReadOnlyTask task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(TaskCard.load(task, mainTaskList.indexOf(task) + 1).getLayout()); + } + } + } +} +``` +###### /java/seedu/agendum/ui/HelpWindow.java +``` java +/** + * Controller for help anchorpane + */ +public class HelpWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); + private static final String FXML = "HelpWindow.fxml"; + private static final int PADDING = 11; + private static final double COMMAND_COLUMN_WIDTH = 0.2; + private static final double DESCRIPTION_COLUMN_WIDTH = 0.4; + private static final double FORMAT_COLUMN_WIDTH = 0.4; + private final ObservableList> commandList = FXCollections.observableArrayList(); + + private enum CommandColumns { + COMMAND, DESCRIPTION, FORMAT + } + + @FXML + private AnchorPane helpWindowRoot; + + @FXML + private TableView> commandTable; + + @FXML + private TableColumn, String> commandColumn; + + @FXML + private TableColumn, String> descriptionColumn; + + @FXML + private TableColumn, String> formatColumn; + + private StackPane messagePlaceHolder; + private AnchorPane mainPane; + + /** + * Initializes the controller class. This method is automatically called + * after the fxml file has been loaded. + */ + @FXML + private void initialize() { + + commandColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().get(CommandColumns.COMMAND))); + descriptionColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().get(CommandColumns.DESCRIPTION))); + formatColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().get(CommandColumns.FORMAT))); + commandTable.setItems(commandList); + commandTable.setEditable(false); + } + + public static HelpWindow load(Stage primaryStage, StackPane messagePlaceHolder) { + logger.fine("Showing help page about the application."); + HelpWindow helpWindow = UiPartLoader.loadUiPart(primaryStage, new HelpWindow()); + helpWindow.configure(messagePlaceHolder); + return helpWindow; + } + + @Override + public void setNode(Node node) { + this.mainPane = (AnchorPane)node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + public AnchorPane getMainPane() { + return this.mainPane; + } + + private void configure(StackPane messagePlaceHolder){ + this.messagePlaceHolder = messagePlaceHolder; + commandColumn.prefWidthProperty().bind(commandTable.widthProperty().multiply(COMMAND_COLUMN_WIDTH)); + descriptionColumn.prefWidthProperty().bind(commandTable.widthProperty().multiply(DESCRIPTION_COLUMN_WIDTH)); + formatColumn.prefWidthProperty().bind(commandTable.widthProperty().multiply(FORMAT_COLUMN_WIDTH)); + loadHelpList(); + } + + public void show(double height) { + this.messagePlaceHolder.setPadding(new Insets(PADDING)); + this.helpWindowRoot.setMinHeight(height); + this.messagePlaceHolder.setPrefSize(Control.USE_COMPUTED_SIZE, Control.USE_COMPUTED_SIZE); + this.messagePlaceHolder.getChildren().add(helpWindowRoot); + } + +``` +###### /java/seedu/agendum/ui/MainWindow.java +``` java +/** + * The Main Window. Provides the basic application layout containing a menu bar + * and space where other JavaFX elements can be placed. + */ +public class MainWindow extends UiPart { + private static final Logger logger = LogsCenter.getLogger(MainWindow.class); + + private static final String ICON = "/images/agendum_icon.png"; + private static final String FXML = "MainWindow.fxml"; + private static final String LIST_COMMAND = "list"; + private static final String UNDO_COMMAND = "undo"; + + private Logic logic; + + // Independent Ui parts residing in this Ui container + private TasksPanel upcomingTasksPanel; + private TasksPanel completedTasksPanel; + private TasksPanel floatingTasksPanel; + private AnchorPane helpWindow; + private ResultPopUp resultPopUp; + private StatusBarFooter statusBarFooter; + private CommandBox commandBox; + private Config config; + private UserPrefs userPrefs; + + // Handles to elements of this Ui container + private VBox rootLayout; + private Scene scene; + + @FXML + private AnchorPane browserPlaceholder; + + @FXML + private AnchorPane commandBoxPlaceholder; + + @FXML + private MenuItem helpMenuItem; + + @FXML + private SplitPane splitPane; + + @FXML + private AnchorPane upcomingTasksPlaceHolder; + + @FXML + private AnchorPane completedTasksPlaceHolder; + + @FXML + private AnchorPane floatingTasksPlaceHolder; + + @FXML + private AnchorPane statusbarPlaceholder; + + @FXML + private StackPane messagePlaceHolder; + + public MainWindow() { + super(); + } + + @Override + public void setNode(Node node) { + rootLayout = (VBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + public static MainWindow load(Stage primaryStage, Config config, UserPrefs prefs, Logic logic) { + MainWindow mainWindow = UiPartLoader.loadUiPart(primaryStage, new MainWindow()); + mainWindow.configure(config.getAppTitle(), config, prefs, logic); + return mainWindow; + } + + private void configure(String appTitle, Config config, UserPrefs prefs, Logic logic) { + + this.logic = logic; + this.config = config; + this.userPrefs = prefs; + + setTitle(appTitle); + setIcon(ICON); + setWindowDefaultSize(prefs); + scene = new Scene(rootLayout); + primaryStage.setScene(scene); + primaryStage.setOnCloseRequest(e -> Platform.exit()); + configureEscape(); + configureHelpWindowToggle(); + } + + /** + * Set shortcut key to switch between help window and main window + */ + private void configureHelpWindowToggle() { + scene.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler() { + KeyCombination toggleHelpWindow = new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN); + KeyCombination undo = new KeyCodeCombination(KeyCode.Z, KeyCombination.CONTROL_DOWN); + @Override + public void handle(KeyEvent evt) { + if (toggleHelpWindow.match(evt) && messagePlaceHolder.getChildren().size() == 0) { + openHelpWindow(); + } else if (toggleHelpWindow.match(evt) && messagePlaceHolder.getChildren().contains(helpWindow)) { + closeHelpWindow(); + } else if(undo.match(evt)) { + logic.execute(UNDO_COMMAND); + } + } + }); + } + + /** + * Set shortcut key to quickly switch back to main list after using find + * command or showing help page + */ + private void configureEscape() { + scene.addEventFilter(KeyEvent.KEY_PRESSED, evt -> { + if (evt.getCode().equals(KeyCode.ESCAPE) && messagePlaceHolder.getChildren().contains(helpWindow)) { + closeHelpWindow(); + } else if(evt.getCode().equals(KeyCode.ESCAPE) && messagePlaceHolder.getChildren().size() > 0) { + messagePlaceHolder.getChildren().clear(); + messagePlaceHolder.setMaxHeight(0); + logic.execute(LIST_COMMAND); + } + }); + } + + /** + * Loads the ui elements + */ + public void fillInnerParts() { + logger.info("loading ui elements"); + upcomingTasksPanel = UpcomingTasksPanel.load(primaryStage, getUpcomingTasksPlaceHolder(), + logic.getFilteredTaskList(), new UpcomingTasksPanel()); + completedTasksPanel = CompletedTasksPanel.load(primaryStage, getCompletedTasksPlaceHolder(), + logic.getFilteredTaskList(), new CompletedTasksPanel()); + floatingTasksPanel = FloatingTasksPanel.load(primaryStage, getFloatingTasksPlaceHolder(), + logic.getFilteredTaskList(), new FloatingTasksPanel()); + resultPopUp = ResultPopUp.load(primaryStage); + statusBarFooter = StatusBarFooter.load(primaryStage, getStatusbarPlaceholder(), config.getToDoListFilePath()); + commandBox = CommandBox.load(primaryStage, getCommandBoxPlaceholder(), messagePlaceHolder, resultPopUp, logic); + } + + public AnchorPane getCommandBoxPlaceholder() { + return commandBoxPlaceholder; + } + + public StackPane getMessagePlaceHolder() { + return messagePlaceHolder; + } + + public AnchorPane getStatusbarPlaceholder() { + return statusbarPlaceholder; + } + + public AnchorPane getUpcomingTasksPlaceHolder() { + return upcomingTasksPlaceHolder; + } + + public AnchorPane getCompletedTasksPlaceHolder() { + return completedTasksPlaceHolder; + } + + public AnchorPane getFloatingTasksPlaceHolder() { + return floatingTasksPlaceHolder; + } + + public UpcomingTasksPanel getUpcomingTasksPanel() { + return (UpcomingTasksPanel) this.upcomingTasksPanel; + } + + public CompletedTasksPanel getCompletedTasksPanel() { + return (CompletedTasksPanel) this.completedTasksPanel; + } + + public FloatingTasksPanel getFloatingasksPanel() { + return (FloatingTasksPanel) this.floatingTasksPanel; + + } + +``` +###### /java/seedu/agendum/ui/MainWindow.java +``` java + @FXML + public void handleHelp() { + if(!messagePlaceHolder.getChildren().contains(helpWindow)) { + openHelpWindow(); + } + + } + + public void openHelpWindow() { + HelpWindow helpWindow = HelpWindow.load(primaryStage, messagePlaceHolder); + this.helpWindow = helpWindow.getMainPane(); + helpWindow.show(upcomingTasksPlaceHolder.getHeight()); + rootLayout.getChildren().remove(rootLayout.getChildren().indexOf(splitPane)); + } + + public void closeHelpWindow() { + messagePlaceHolder.getChildren().clear(); + messagePlaceHolder.setMaxHeight(0); + messagePlaceHolder.setPadding(new Insets(0)); + if(!rootLayout.getChildren().contains(splitPane)) { + rootLayout.getChildren().add(rootLayout.getChildren().indexOf(statusbarPlaceholder), splitPane); + } + } + + public void hide() { + primaryStage.hide(); + } + + public void show() { + primaryStage.show(); + } + + /** + * Closes the application. + */ + @FXML + private void handleExit() { + raise(new ExitAppRequestEvent()); + } + +} +``` +###### /java/seedu/agendum/ui/ResultPopUp.java +``` java +/** + * Controller for a pop up window that shows command execution result + */ +public class ResultPopUp extends UiPart { + private static final Logger logger = LogsCenter.getLogger(ResultPopUp.class); + private static final String FXML = "ResultPopUp.fxml"; + private static Stage root; + private AnchorPane mainPane; + private Stage dialogStage; + + private final PauseTransition delay = new PauseTransition(Duration.seconds(5)); + + @FXML + private Label resultDisplay; + + public static ResultPopUp load(Stage primaryStage) { + logger.fine("Showing command execution result."); + root = primaryStage; + ResultPopUp resultPopUp = UiPartLoader.loadUiPart(primaryStage, new ResultPopUp()); + resultPopUp.configure(); + return resultPopUp; + } + + @Override + public void setNode(Node node) { + mainPane = (AnchorPane) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + private void configure() { + + Scene scene = new Scene(mainPane); + + dialogStage = createDialogStage(null, null, scene); + dialogStage.initModality(Modality.NONE); + dialogStage.setAlwaysOnTop(true); + dialogStage.setOnShown((e1) -> primaryStage.requestFocus()); + + scene.setFill(Color.TRANSPARENT); + dialogStage.initStyle(StageStyle.TRANSPARENT); + + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + dialogStage.setMaxHeight(screenSize.getWidth()); + dialogStage.setMaxWidth(screenSize.getHeight()); + } + + private boolean isShowingMessage() { + return dialogStage.isShowing() && dialogStage.getOpacity() != 0; + } + + /** + * Shows message in a pop up window for several seconds + * @param message The command execution result to be shown + */ + public void postMessage(String message) { + + if(this.isShowingMessage()) { + delay.playFromStart(); + } else { + delay.setOnFinished(event -> dialogStage.setOpacity(0)); + delay.play(); + } + + resultDisplay.setWrapText(true); + resultDisplay.setText(message); + show(); + } + + private void show() { + dialogStage.setOpacity(1.0); + dialogStage.sizeToScene(); + dialogStage.show(); + dialogStage.setX(root.getX() + root.getWidth() / 2 - dialogStage.getWidth() / 2); + dialogStage.setY(root.getY() + root.getHeight() / 2 - dialogStage.getHeight() / 2); + } +} +``` +###### /java/seedu/agendum/ui/StatusBarFooter.java +``` java + private void addTimeStatus() { + Label timeStatus = new DigitalClock(); + FxViewUtil.applyAnchorBoundaryParameters(timeStatus, 0.0, 0.0, 0.0, 0.0); + timeStatus.setAlignment(Pos.CENTER); + timeStatusBarPane.getChildren().add(timeStatus); + } + + @Override + public void setNode(Node node) { + mainPane = (GridPane) node; + } + + @Override + public void setPlaceholder(AnchorPane placeholder) { + this.placeHolder = placeholder; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + @Subscribe + public void handleToDoListChangedEvent(ToDoListChangedEvent event) { + String lastUpdated = (new Date()).toString(); + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Setting last updated status to " + lastUpdated)); + setSyncStatus("Last Updated: " + lastUpdated); + } + +``` +###### /java/seedu/agendum/ui/StatusBarFooter.java +``` java +class DigitalClock extends Label { + + private static final String DATE_TIME_PATTERN = "HH:mm, EEE d MMM yyyy"; + + public DigitalClock() { + bindToTime(); + } + + private void bindToTime() { + Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(0), actionEvent -> { + Calendar time = Calendar.getInstance(); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_TIME_PATTERN); + setText(simpleDateFormat.format(time.getTime())); + setTextFill(Color.web("#ffffff")); + }), new KeyFrame(Duration.seconds(1))); + + timeline.setCycleCount(Animation.INDEFINITE); + timeline.play(); + } +} +``` +###### /java/seedu/agendum/ui/TaskCard.java +``` java +public class TaskCard extends UiPart { + + private static final String FXML = "TaskCard.fxml"; + private static final String OVERDUE_PREFIX = "Overdue\n"; + private static final String COMPLETED_PREFIX = "Completed on "; + private static final String TASK_TIME_PATTERN = "HH:mm EEE, dd MMM"; + private static final String COMPLETED_TIME_PATTERN = "EEE, dd MMM"; + private static final String START_TIME_PREFIX = "from "; + private static final String END_TIME_PREFIX = " to "; + private static final String DEADLINE_PREFIX = "by "; + private static final String EMPTY_PREFIX = ""; + private static final String OVERDUE_STYLE = "-fx-background-color: rgba(244, 67, 54, 0.8)"; + private static final String UPCOMING_STYLE = "-fx-background-color: rgba(255, 235, 59, 0.8)"; + private static final String OTHER_STYLE = "-fx-background-color: rgba(255,255,255,0.6)"; + private static final Color NAME_COLOR_DARK = Color.web("#3a3d42"); + private static final Color TIME_COLOR_DARK = Color.web("#4172c1"); + private static final Color NAME_COLOR_LIGHT = Color.web("#ffffff"); + private static final Color TIME_COLOR_LIGHT = Color.web("#fff59d"); + + @FXML + private HBox cardPane; + @FXML + private VBox taskVbox; + @FXML + private Label name; + @FXML + private Label id; + + private ReadOnlyTask task; + private String displayedIndex; + + public TaskCard() { + } + + public static TaskCard load(ReadOnlyTask task, int Index) { + TaskCard card = new TaskCard(); + card.task = task; + card.displayedIndex = String.valueOf(Index) + "."; + return UiPartLoader.loadUiPart(card); + } + + @FXML + public void initialize() { + + Label time = new Label(); + time.setId("time"); + + if (task.isOverdue()) { + cardPane.setStyle(OVERDUE_STYLE); + name.setTextFill(NAME_COLOR_LIGHT); + time.setTextFill(TIME_COLOR_LIGHT); + id.setTextFill(NAME_COLOR_LIGHT); + } else if (task.isUpcoming()) { + cardPane.setStyle(UPCOMING_STYLE); + name.setTextFill(NAME_COLOR_DARK); + time.setTextFill(TIME_COLOR_DARK); + } else { + cardPane.setStyle(OTHER_STYLE); + name.setTextFill(NAME_COLOR_DARK); + time.setTextFill(TIME_COLOR_DARK); + } + + StringBuilder timeDescription = new StringBuilder(); + timeDescription.append(formatTaskTime(task)); + + if (task.isCompleted()) { + timeDescription.append(formatUpdatedTime(task)); + } + + name.setText(task.getName().fullName); + id.setText(displayedIndex); + time.setText(timeDescription.toString()); + time.setMaxHeight(Control.USE_COMPUTED_SIZE); + time.setWrapText(true); + + if (task.hasTime() || task.isCompleted()) { + taskVbox.getChildren().add(time); + taskVbox.setAlignment(Pos.CENTER_LEFT); + time.setAlignment(Pos.CENTER_LEFT); + time.setFont(Font.font("Verdana", FontPosture.ITALIC, 11)); + } + } + + private String formatTime(String dateTimePattern, String prefix, Optional dateTime) { + + StringBuilder sb = new StringBuilder(); + DateTimeFormatter format = DateTimeFormatter.ofPattern(dateTimePattern); + sb.append(prefix).append(dateTime.get().format(format)); + + return sb.toString(); + } + + private String formatTaskTime(ReadOnlyTask task) { + + StringBuilder timeStringBuilder = new StringBuilder(); + + if (task.isOverdue()) { + timeStringBuilder.append(OVERDUE_PREFIX); + } + + if (task.isEvent()) { + String startTime = formatTime(TASK_TIME_PATTERN, START_TIME_PREFIX, task.getStartDateTime()); + String endTime = formatTime(TASK_TIME_PATTERN, END_TIME_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(startTime); + timeStringBuilder.append(endTime); + } else if (task.hasDeadline()) { + String deadline = formatTime(TASK_TIME_PATTERN, DEADLINE_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(deadline); + } + + return timeStringBuilder.toString(); + } + + private String formatUpdatedTime(ReadOnlyTask task) { + StringBuilder timeStringBuilder = new StringBuilder(); + if (task.hasTime()) { + timeStringBuilder.append("\n"); + } + timeStringBuilder.append(COMPLETED_PREFIX); + timeStringBuilder.append(formatTime(COMPLETED_TIME_PATTERN, EMPTY_PREFIX, + Optional.ofNullable(task.getLastUpdatedTime()))); + return timeStringBuilder.toString(); + } + + public HBox getLayout() { + return cardPane; + } + + @Override + public void setNode(Node node) { + cardPane = (HBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } +} +``` +###### /java/seedu/agendum/ui/TasksPanel.java +``` java +/** + * Panel that contains the list of tasks + */ +public abstract class TasksPanel extends UiPart{ + private AnchorPane panel; + private AnchorPane placeHolderPane; + + public TasksPanel() { + super(); + } + + @Override + public void setNode(Node node) { + panel = (AnchorPane) node; + } + + @Override + public void setPlaceholder(AnchorPane pane) { + this.placeHolderPane = pane; + } + + public static TasksPanel load(Stage primaryStage, AnchorPane tasksPlaceholder, + ObservableList taskList, TasksPanel tasksPanelType) { + TasksPanel tasksPanel = UiPartLoader.loadUiPart(primaryStage, tasksPlaceholder, tasksPanelType); + tasksPanel.configure(taskList); + return tasksPanel; + } + + private void configure(ObservableList taskList) { + setConnections(taskList); + addToPlaceholder(); + } + + private void addToPlaceholder() { + SplitPane.setResizableWithParent(placeHolderPane, false); + placeHolderPane.getChildren().add(panel); + } + + protected abstract void setConnections(ObservableList allTasks); + + public abstract void scrollTo(Task task, boolean hasMultipleTasks); + +} +``` +###### /java/seedu/agendum/ui/UpcomingTasksPanel.java +``` java +/** + * Panel contains the list of all uncompleted tasks with time + */ +public class UpcomingTasksPanel extends TasksPanel { + private static final String FXML = "UpcomingTasksPanel.fxml"; + private static ObservableList mainTaskList; + private MultipleSelectionModel selectionModel; + + @FXML + private ListView upcomingTasksListView; + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + protected void setConnections(ObservableList taskList) { + mainTaskList = taskList; + upcomingTasksListView.setItems(taskList.filtered(task -> task.hasTime() && !task.isCompleted())); + upcomingTasksListView.setCellFactory(listView -> new upcomingTasksListViewCell()); + configure(); + } + + private void configure() { + selectionModel = upcomingTasksListView.getSelectionModel(); + upcomingTasksListView.setSelectionModel(null); + upcomingTasksListView.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume); + } + + /** + * Scrolls to the newly updated task and highlight for several seconds. If + * there are multiple tasks updated, previous highlight will not be cleared. + */ + public void scrollTo(Task task, boolean hasMultipleTasks) { + Platform.runLater(() -> { + + int index = mainTaskList.indexOf(task); + upcomingTasksListView.scrollTo(index); + upcomingTasksListView.setSelectionModel(selectionModel); + + if(hasMultipleTasks) { + upcomingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + upcomingTasksListView.getSelectionModel().select(index); + } else { + upcomingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + upcomingTasksListView.getSelectionModel().clearAndSelect(index); + } + + PauseTransition delay = new PauseTransition(Duration.seconds(5)); + delay.setOnFinished(event -> upcomingTasksListView.getSelectionModel().clearSelection(index)); + delay.play(); + }); + } + + class upcomingTasksListViewCell extends ListCell { + + public upcomingTasksListViewCell() { + prefWidthProperty().bind(upcomingTasksListView.widthProperty()); + setMaxWidth(Control.USE_PREF_SIZE); + } + + @Override + protected void updateItem(ReadOnlyTask task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(TaskCard.load(task, mainTaskList.indexOf(task) + 1).getLayout()); + } + } + } + +} +``` +###### /resources/view/CompletedTasksPanel.fxml +``` fxml + + + + + + + + + + + + + + + + + + + + +``` +###### /resources/view/DarkTheme.css +``` css +.background { + -fx-background-color: derive(#263238, 20%); +} + +.text-field { + -fx-font-size: 12pt; + -fx-font-family: "Segoe UI Semibold"; +} + +.tab-pane { + -fx-padding: 0 0 0 1; +} + +.tab-pane .tab-header-area { + -fx-padding: 0 0 0 0; + -fx-min-height: 0; + -fx-max-height: 0; +} + +.split-pane:horizontal .split-pane-divider { + -fx-border-color: transparent; + -fx-background-color: transparent; +} + +.split-pane { + -fx-border-radius: 1; + -fx-border-width: 1; + -fx-background-color: derive(#263238, 20%); +} + +.anchor-pane { + -fx-background-color: derive(#263238, 20%); +} + +.anchor-pane-with-border { + -fx-background-color: derive(#263238, 20%); + -fx-border-color: transparent; + -fx-border-top-width: 1px; +} + +.stack-pane { + -fx-background-color: derive(#263238, 20%); + -fx-border-color: transparent; + -fx-border-top-width: 1px; +} + +.status-bar { + -fx-background-color: derive(#263238, 20%); + -fx-border-color: transparent; + -fx-text-fill: white; +} + +.result-display { + -fx-background-color: #ffffff; +} + +.result-display { + -fx-text-fill: black; +} + +.status-bar .label { + -fx-background-color: transparent; + -fx-text-fill: white; +} + +.status-bar-with-border { + -fx-background-color: derive(#263238, 30%); + -fx-border-color: transparent; + -fx-border-width: 1px; +} + +.status-bar-with-border .label { + -fx-text-fill: white; +} + +.grid-pane { + -fx-background-color: derive(#263238, 30%); + -fx-border-color: derive(#263238, 30%); + -fx-border-width: 1px; +} + +.grid-pane .anchor-pane { + -fx-background-color: derive(#263238, 20%); +} + +.context-menu { + -fx-background-color: derive(#263238, 50%); +} + +.context-menu .label { + -fx-text-fill: white; +} + +.menu-bar { + -fx-background-color: derive(#263238, 20%); +} + +.menu-bar .label { + -fx-font-size: 14pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-opacity: 0.9; +} + +.menu .left-container { + -fx-background-color: black; +} + +/* + * Metro style Push Button + * Author: Pedro Duque Vieira + * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ + */ +.button { + -fx-padding: 5 22 5 22; + -fx-border-color: #e2e2e2; + -fx-border-width: 2; + -fx-background-radius: 0; + -fx-background-color: #263238; + -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: #d8d8d8; + -fx-background-insets: 0 0 0 0, 0, 1, 2; +} + +.button:hover { + -fx-background-color: #3a3a3a; +} + +.button:pressed, .button:default:hover:pressed { + -fx-background-color: white; + -fx-text-fill: #263238; +} + +.button:focused { + -fx-border-color: white, white; + -fx-border-width: 1, 1; + -fx-border-style: solid, segments(1, 1); + -fx-border-radius: 0, 0; + -fx-border-insets: 1 1 1 1, 0; +} + +.button:disabled, .button:default:disabled { + -fx-opacity: 0.4; + -fx-background-color: #263238; + -fx-text-fill: white; +} + +.button:default { + -fx-background-color: -fx-focus-color; + -fx-text-fill: #ffffff; +} + +.button:default:hover { + -fx-background-color: derive(-fx-focus-color, 30%); +} + +.dialog-pane { + -fx-background-color: #263238; +} + +.dialog-pane > *.button-bar > *.container { + -fx-background-color: #263238; +} + +.dialog-pane > *.label.content { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: white; +} + +.dialog-pane:header *.header-panel { + -fx-background-color: derive(#263238, 25%); +} + +.dialog-pane:header *.header-panel *.label { + -fx-font-size: 18px; + -fx-font-style: italic; + -fx-fill: white; + -fx-text-fill: white; +} + +.list-view .scroll-bar:vertical .increment-arrow, +.list-view .scroll-bar:vertical .decrement-arrow, +.list-view .scroll-bar:vertical .increment-button, +.list-view .scroll-bar:vertical .decrement-button { + -fx-padding:0; +} + +.list-view .scroll-bar:horizontal .increment-arrow, +.list-view .scroll-bar:horizontal .decrement-arrow, +.list-view .scroll-bar:horizontal .increment-button, +.list-view .scroll-bar:horizontal .decrement-button { + -fx-padding:0; +} + +.list-view { + -fx-background-radius: 10; +} + +#cardPane { + -fx-background-color: transparent; +} + +#commandTypeLabel { + -fx-font-size: 11px; + -fx-text-fill: #F70D1A; +} + +#filterField, #taskListPanel, #taskWebpage { + -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); +} +``` +###### /resources/view/FloatingTasksPanel.fxml +``` fxml + + + + + + + + + + + + + + + + + + + + +``` +###### /resources/view/HelpWindow.css +``` css + +.pane { + -fx-background-color: #EA6254; + -fx-background-radius: 10; +} + +.button { + -fx-background-color: rgba(0,0,0,0.1); + -fx-background-radius: 10; +} + +.button:hover { + -fx-background-color: rgba(0,0,0,0.3); + -fx-background-radius: 10; +} + +.table-view:focused{ + -fx-background-color: transparent; +} + +.table-view{ + -fx-background-color: transparent; +} + +.table-view .column-header-background{ + -fx-background-color: transparent; +} + +.table-view .column-header-background .label{ + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-font-size: 16pt; +} + +.table-view .column-header { + -fx-background-color: transparent; +} + +.table-view .table-cell{ + -fx-text-fill: white; + -fx-font-size: 14pt; +} + +.table-cell:empty { + -fx-background-color: transparent; +} + +.table-row-cell{ + -fx-table-cell-border-color: transparent; + -fx-background-color: transparent; +} + +.table-row-cell:even{ + -fx-table-cell-border-color: transparent; + -fx-background-color: rgba(255,255,255,0.3); +} + +.table-row-cell:hover{ + -fx-background-color: rgba(0,0,0,0.1); +} + +.scroll-bar .increment-button { + -fx-opacity: 0; +} + +.scroll-bar .decrement-button { + -fx-opacity: 0; +} + +.scroll-bar { + -fx-background-color: transparent; +} + +.scroll-bar .track-background { + -fx-opacity: 0; + -fx-background-color: transparent; + -fx-background-insets: 0; +} + +.scroll-bar .track { + -fx-opacity: 0; + -fx-background-color: transparent; + -fx-border-color:transparent; +} + +.scroll-bar .thumb { + -fx-background-color: transparent; + -fx-background-insets: 4 0 4 0; + -fx-background-radius: 2em; +} + +.table-view .scroll-bar:vertical .increment-arrow, +.table-view .scroll-bar:vertical .decrement-arrow, +.table-view .scroll-bar:vertical .increment-button, +.table-view .scroll-bar:vertical .decrement-button { + -fx-padding:0; +} + +.table-view .scroll-bar:horizontal .increment-arrow, +.table-view .scroll-bar:horizontal .decrement-arrow, +.table-view .scroll-bar:horizontal .increment-button, +.table-view .scroll-bar:horizontal .decrement-button { + -fx-padding:0; +} +``` +###### /resources/view/HelpWindow.fxml +``` fxml + + + + + + + + + + + + + + + + + + + + + + +``` +###### /resources/view/MainWindow.fxml +``` fxml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` +###### /resources/view/ResultPopUp.css +``` css +.pane { + -fx-background-color: rgb(229,74,63); + -fx-background-radius: 20; + -fx-padding: 10px; +} + +.label { + -fx-text-fill: white; + -fx-font-size: 14pt; +} +``` +###### /resources/view/ResultPopUp.fxml +``` fxml + + + + + +``` +###### /resources/view/StatusBarFooter.fxml +``` fxml + + + + + + + + + + + + + + + +``` +###### /resources/view/TaskCard.fxml +``` fxml + + + + + + + + + + + + + + + + + + + + + + +``` +###### /resources/view/TasksPanel.css +``` css +.all-pane { + -fx-background-color: #4DB6AC; + -fx-background-radius: 10; +} + +.completed-pane { + -fx-background-color: #727a87; + -fx-background-radius: 10; +} + +.other-pane { + -fx-background-color: #3498DB; + -fx-background-radius: 10; +} + +.list-view { + -fx-background-color: transparent; + -fx-background-radius: 10; +} + +.list-cell { + -fx-background-color: transparent; + -fx-background-radius: 10; +} + +.list-cell:empty { + -fx-background-color: transparent; +} + +.list-cell:filled:selected:focused, .list-cell:filled:selected { + -fx-background-color: #3949AB; + -fx-text-fill: red; +} + +.cell_big_label { + -fx-font-size: 14pt; + -fx-opacity: 0.9; +} + +.cell_small_label { + -fx-text-fill: #3a3d42; + -fx-opacity: 0.9; +} + +.hbox { + -fx-background-radius: 10; +} + +.vbox { + -fx-background-color: transparent; +} +``` +###### /resources/view/UpcomingTasksPanel.fxml +``` fxml + + + + + + + + + + + + + + + + + + + + +``` diff --git a/collated/main/A0148095X.md b/collated/main/A0148095X.md new file mode 100644 index 000000000000..f97ad3d76c58 --- /dev/null +++ b/collated/main/A0148095X.md @@ -0,0 +1,443 @@ +# A0148095X +###### /java/seedu/agendum/commons/events/model/ChangeSaveLocationEvent.java +``` java +/** Indicates a request from model to change the save location of the data file*/ +public class ChangeSaveLocationEvent extends BaseEvent { + + public final String location; + + private String message; + + public ChangeSaveLocationEvent(String saveLocation){ + this.location = saveLocation; + this.message = "Request to change save location to: " + location; + } + + @Override + public String toString() { + return message; + } +} +``` +###### /java/seedu/agendum/commons/events/model/LoadDataRequestEvent.java +``` java +/** Indicates a request from model to load data **/ +public class LoadDataRequestEvent extends BaseEvent { + + public final String loadLocation; + + public LoadDataRequestEvent(String loadLocation){ + this.loadLocation = loadLocation; + } + + @Override + public String toString() { + return "Request to load from: " + loadLocation; + } +} +``` +###### /java/seedu/agendum/commons/events/storage/DataLoadingExceptionEvent.java +``` java +/** Indicates an exception during a file loading **/ +public class DataLoadingExceptionEvent extends BaseEvent { + + public Exception exception; + + public DataLoadingExceptionEvent(Exception exception) { + this.exception = exception; + } + + @Override + public String toString(){ + return exception.toString(); + } + +} +``` +###### /java/seedu/agendum/commons/events/storage/LoadDataCompleteEvent.java +``` java +/** Indicates the ToDoList load request has completed successfully **/ +public class LoadDataCompleteEvent extends BaseEvent { + + public final ReadOnlyToDoList data; + + private String message; + + public LoadDataCompleteEvent(ReadOnlyToDoList data){ + this.data = data; + this.message = "Todo list data load completed. Task list size: " + data.getTaskList().size(); + } + + @Override + public String toString() { + return message; + } +} +``` +###### /java/seedu/agendum/commons/util/FileUtil.java +``` java + public static void deleteFile(String filePath) throws FileDeletionException { + assert StringUtil.isValidPathToFile(filePath); + + File file = new File(filePath); + if (!file.delete()) { + throw new FileDeletionException("Unable to delete file at: " + filePath); + } + } + + /** Even though a path is valid, it might not exist or the user has insufficient privileges.
+ * i.e. J drive is a valid location, but it does not exist. + * + * Creates and deletes an empty file at the path. + * + * @param path must be a valid file path + * @return true if the path is exists and user has sufficient privileges. + */ + public static boolean isPathAvailable(String path) { + + File file = new File(path); + boolean exists = file.exists(); + + try { + createParentDirsOfFile(file); + file.createNewFile(); + } catch (IOException e) { + return false; + } + + if(!exists) { // prevent deleting an existing file + file.delete(); + } + return true; + } + + public static boolean isFileExists(String filePath) { + File file = new File(filePath); + return isFileExists(file); + } + +``` +###### /java/seedu/agendum/commons/util/StringUtil.java +``` java + /** + * Checks whether the string matches an approved file path. + *

+ * Examples of valid file paths:
+ * - C:/Program Files (x86)/some-folder/data.xml
+ * - data/todolist.xml
+ * - list.xml
+ *

+ *

+ * Examples of invalid file paths:
+ * - data/.xml
+ * - data/user
+ * - C:/Program /data.xml
+ * - C:/ Files/data.xml
+ *

+ * @param s should be trimmed + * @return true if it is a valid file path + */ + public static boolean isValidPathToFile(String s) { + return s != null && !s.isEmpty() && s.matches("([A-z]\\:)?(\\/?[\\w-_()]+(\\s[\\w-_()])?)+(\\.[\\w]+)"); + } +} +``` +###### /java/seedu/agendum/commons/util/XmlUtil.java +``` java + public static boolean isFileCorrectFormat(String filePath) { + File file = new File(filePath); + try { + getDataFromFile(file, XmlSerializableToDoList.class); + return true; + } catch (Exception e) { + return false; + } + } + +``` +###### /java/seedu/agendum/logic/commands/LoadCommand.java +``` java +/** Allow the user to load a file in the correct todolist format **/ +public class LoadCommand extends Command { + + public static final String COMMAND_WORD = "load"; + public static final String COMMAND_FORMAT = "load "; + public static final String COMMAND_DESCRIPTION = "loads task data from the specified location"; + + public static final String MESSAGE_SUCCESS = "Data successfully loaded from: %1$s"; + public static final String MESSAGE_PATH_INVALID = "The specified path to file is invalid: %1$s"; + public static final String MESSAGE_FILE_DOES_NOT_EXIST = "The specified file does not exist: %1$s"; + public static final String MESSAGE_FILE_WRONG_FORMAT = "The specified file is in the wrong format: %1$s"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + "agendum/todolist.xml"; + + private String pathToFile; + + public LoadCommand(String pathToFile) { + this.pathToFile = pathToFile.trim(); + } + + @Override + public CommandResult execute() { + assert pathToFile != null; + + if(!isValidPathToFile()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(String.format(MESSAGE_PATH_INVALID, pathToFile)); + } + + if(!isFileExists()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(String.format(MESSAGE_FILE_DOES_NOT_EXIST, pathToFile)); + } + + if(!isFileCorrectFormat()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(String.format(MESSAGE_FILE_WRONG_FORMAT, pathToFile)); + } + + model.loadFromLocation(pathToFile); + return new CommandResult(String.format(MESSAGE_SUCCESS, pathToFile)); + } + + private boolean isFileCorrectFormat() { + return XmlUtil.isFileCorrectFormat(pathToFile); + } + + private boolean isValidPathToFile() { + return StringUtil.isValidPathToFile(pathToFile); + } + + private boolean isFileExists() { + return FileUtil.isFileExists(pathToFile); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + + +} +``` +###### /java/seedu/agendum/logic/commands/StoreCommand.java +``` java +/** Allow the user to specify a folder as the data storage location **/ +public class StoreCommand extends Command { + + public static final String COMMAND_WORD = "store"; + public static final String COMMAND_FORMAT = "store "; + public static final String COMMAND_DESCRIPTION = "stores task data at specified location"; + public static final String COMMAND_EXAMPLE = "store agendum/todolist.xml"; + + public static final String MESSAGE_SUCCESS = "New save location: %1$s"; + public static final String MESSAGE_LOCATION_DEFAULT = "Save location set to default: %1$s"; + + public static final String MESSAGE_LOCATION_INACCESSIBLE = "The specified location is inaccessible; try running Agendum as administrator."; + public static final String MESSAGE_FILE_EXISTS = "The specified file exists; would you like to use LOAD instead?"; + public static final String MESSAGE_PATH_WRONG_FORMAT = "The specified path is in the wrong format. Example: " + COMMAND_EXAMPLE; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + "agendum/todolist.xml"; + + private String pathToFile; + + public StoreCommand(String location) { + this.pathToFile = location.trim(); + } + + @Override + public CommandResult execute() { + assert pathToFile != null; + + if(pathToFile.equalsIgnoreCase("default")) { // for debug + String defaultLocation = Config.DEFAULT_SAVE_LOCATION; + model.changeSaveLocation(defaultLocation); + return new CommandResult(String.format(MESSAGE_LOCATION_DEFAULT, defaultLocation)); + } + + if(isFileExists()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(MESSAGE_FILE_EXISTS); + } + + if(!isPathCorrectFormat()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(MESSAGE_PATH_WRONG_FORMAT); + } + + if(!isPathAvailable()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(MESSAGE_LOCATION_INACCESSIBLE); + } + + model.changeSaveLocation(pathToFile); + return new CommandResult(String.format(MESSAGE_SUCCESS, pathToFile)); + } + + private boolean isPathCorrectFormat() { + return StringUtil.isValidPathToFile(pathToFile); + } + + private boolean isPathAvailable() { + return FileUtil.isPathAvailable(pathToFile); + } + + private boolean isFileExists() { + return FileUtil.isFileExists(pathToFile); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} +``` +###### /java/seedu/agendum/model/ModelManager.java +``` java + /** Raises an event to indicate that save location has changed */ + private void indicateChangeSaveLocation(String location) { + raise(new ChangeSaveLocationEvent(location)); + } + + /** Raises an event to indicate that save location has changed */ + private void indicateLoadDataRequest(String location) { + raise(new LoadDataRequestEvent(location)); + } + +``` +###### /java/seedu/agendum/model/ModelManager.java +``` java + //=========== Storage Methods ========================================================================== + + @Override + public synchronized void changeSaveLocation(String location){ + assert StringUtil.isValidPathToFile(location); + indicateChangeSaveLocation(location); + indicateToDoListChanged(); + } + + @Override + public synchronized void loadFromLocation(String location) { + assert StringUtil.isValidPathToFile(location); + assert XmlUtil.isFileCorrectFormat(location); + + indicateChangeSaveLocation(location); + indicateLoadDataRequest(location); + } + + private void addTaskToSyncManager(Task task) { + syncManager.addNewEvent(task); + } + + private void removeTaskFromSyncManager(ReadOnlyTask task) { + syncManager.deleteEvent((Task) task); + } +``` +###### /java/seedu/agendum/model/ModelManager.java +``` java + @Override + @Subscribe + public void handleLoadDataCompleteEvent(LoadDataCompleteEvent event) { + this.mainToDoList.resetData(event.data); + indicateToDoListChanged(); + clearAllPreviousToDoLists(); + backupCurrentToDoList(); + logger.info("Loading completed - Todolist updated."); + } +} +``` +###### /java/seedu/agendum/storage/StorageManager.java +``` java + @Override + public void setToDoListFilePath(String filePath){ + assert StringUtil.isValidPathToFile(filePath); + toDoListStorage.setToDoListFilePath(filePath); + logger.info("Setting todo list file path to: " + filePath); + } + + private void saveConfigFile() { + try { + ConfigUtil.saveConfig(config, config.getConfigFilePath()); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + } + +``` +###### /java/seedu/agendum/storage/StorageManager.java +``` java + @Override + @Subscribe + public void handleChangeSaveLocationEvent(ChangeSaveLocationEvent event) { + String location = event.location; + + setToDoListFilePath(location); + config.setToDoListFilePath(location); + saveConfigFile(); + + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + } + + @Override + @Subscribe + public void handleLoadDataRequestEvent(LoadDataRequestEvent event) { + setToDoListFilePath(event.loadLocation); + + Optional toDoListOptional; + ReadOnlyToDoList loadedData = null; + try { + toDoListOptional = readToDoList(); + loadedData = toDoListOptional.get(); + logger.info("Loading successful - " + LogsCenter.getEventHandlingLogMessage(event)); + raise(new LoadDataCompleteEvent(loadedData)); + } catch (DataConversionException dce) { + logger.warning("Loading unsuccessful - Data file not in the correct format. "); + raise(new DataLoadingExceptionEvent(dce)); + } catch (NoSuchElementException nse) { + logger.warning("Loading unsuccessful - File does not exist."); + raise(new DataLoadingExceptionEvent(nse)); + } + } +} +``` +###### /java/seedu/agendum/ui/StatusBarFooter.java +``` java + @Subscribe + public void handleChangeSaveLocationEvent(ChangeSaveLocationEvent event) { + String saveLocation = event.location; + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Setting save location to: " + saveLocation)); + setSaveLocation(saveLocation); + } +} + +``` +###### /java/seedu/agendum/ui/UiManager.java +``` java + @Subscribe + private void handleDataLoadingExceptionEvent(DataLoadingExceptionEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + showFileOperationAlertAndWait("Could not load data", "Could not load data from file", event.exception); + } + +``` diff --git a/collated/test/A0003878Y.md b/collated/test/A0003878Y.md new file mode 100644 index 000000000000..5247bcc84186 --- /dev/null +++ b/collated/test/A0003878Y.md @@ -0,0 +1,339 @@ +# A0003878Y +###### /java/seedu/agendum/logic/DateTimeUtilsTest.java +``` java +public class DateTimeUtilsTest { + + private void assertSameDateAndTime(LocalDateTime dateTime1, LocalDateTime dateTime2) { + assertEquals(dateTime1, dateTime2); + } + + private void assertSameDate(LocalDateTime dateTime1, LocalDateTime dateTime2) { + LocalDateTime diff = dateTime1.minusHours(dateTime2.getHour()).minusMinutes(dateTime2.getMinute()).minusSeconds(dateTime2.getSecond()); + assertEquals(dateTime1, diff); + } + + @Test + public void parseNaturalLanguageDateTimeString_dateString_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("2016/01/01"); + assertSameDate(t.get(), LocalDateTime.of(2016,1,1,0,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_dateStringWith24HRTime_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("2016/01/01 01:00"); + assertSameDateAndTime(t.get(), LocalDateTime.of(2016,1,1,1,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_dateStringWithPMTime_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("2016/01/01 3pm"); + assertSameDateAndTime(t.get(), LocalDateTime.of(2016,1,1,15,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_verboseDateString_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("january 10 2017"); + assertSameDate(t.get(), LocalDateTime.of(2017,1,10,0,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_verboseDateStringWithTime_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("january 10 2017 5:15pm"); + assertSameDateAndTime(t.get(), LocalDateTime.of(2017,1,10,17,15)); + } + + @Test + public void balanceStartEndDateTime_startDateAfterEndDate_startDateBeforeEndDate() throws Exception { + LocalDateTime start = LocalDateTime.now(); + LocalDateTime end = start; + + start = start.plusDays(1); + end = start.plusHours(1); + + end = DateTimeUtils.balanceStartAndEndDateTime(start, end); + assertSameDateAndTime(end, start.plusHours(1)); + } + +``` +###### /java/seedu/agendum/logic/EditDistanceCalculatorTest.java +``` java +public class EditDistanceCalculatorTest { + + @Test + public void closestCommandMatch_incorrectCommand_correctCommand() throws Exception { + assertEquals(EditDistanceCalculator.closestCommandMatch("adr").get(), "add"); + assertEquals(EditDistanceCalculator.closestCommandMatch("marc").get(), "mark"); + assertEquals(EditDistanceCalculator.closestCommandMatch("markk").get(), "mark"); + assertEquals(EditDistanceCalculator.closestCommandMatch("storee").get(), "store"); + assertEquals(EditDistanceCalculator.closestCommandMatch("daletr").get(), "delete"); + assertEquals(EditDistanceCalculator.closestCommandMatch("hell").get(), "help"); + assertEquals(EditDistanceCalculator.closestCommandMatch("shdule").get(), "schedule"); + assertEquals(EditDistanceCalculator.closestCommandMatch("rname").get(), "rename"); + } + + @Test + public void closestCommandMatch_incorrectCommand_invalidCommand() throws Exception { + assertEquals(EditDistanceCalculator.closestCommandMatch("asdfohasdf"), Optional.empty()); + assertEquals(EditDistanceCalculator.closestCommandMatch("poasdf"), Optional.empty()); + assertEquals(EditDistanceCalculator.closestCommandMatch("teyu6578"), Optional.empty()); + } + + @Test + public void closestCommandMatch_incompleteCommand_fullCommand() throws Exception { + assertEquals(EditDistanceCalculator.findCommandCompletion("ad").get(), "add"); + assertEquals(EditDistanceCalculator.findCommandCompletion("ma").get(), "mark"); + assertEquals(EditDistanceCalculator.findCommandCompletion("unm").get(), "unmark"); + assertEquals(EditDistanceCalculator.findCommandCompletion("und").get(), "undo"); + assertEquals(EditDistanceCalculator.findCommandCompletion("st").get(), "store"); + assertEquals(EditDistanceCalculator.findCommandCompletion("de").get(), "delete"); + assertEquals(EditDistanceCalculator.findCommandCompletion("he").get(), "help"); + assertEquals(EditDistanceCalculator.findCommandCompletion("sc").get(), "schedule"); + assertEquals(EditDistanceCalculator.findCommandCompletion("r").get(), "rename"); + } + + @Test + public void closestCommandMatch_incompleteCommand_invalidCommand() throws Exception { + assertEquals(EditDistanceCalculator.findCommandCompletion("un"), Optional.empty()); + assertEquals(EditDistanceCalculator.findCommandCompletion("iasdugfiasd"), Optional.empty()); + } + +} +``` +###### /java/seedu/agendum/sync/SyncManagerTests.java +``` java +public class SyncManagerTests { + private SyncManager syncManager; + private SyncProvider mockSyncProvider; + + @Before + public void setUp() { + mockSyncProvider = mock(SyncProvider.class); + syncManager = new SyncManager(mockSyncProvider); + } + + @Test + public void syncManager_setStatusRunning_expectRunning() { + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + assertEquals(syncManager.getSyncStatus(),Sync.SyncStatus.RUNNING); + } + + @Test + public void syncManager_setStatusNotRunning_expectNotRunning() { + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + assertEquals(syncManager.getSyncStatus(),Sync.SyncStatus.NOTRUNNING); + } + + @Test + public void syncManager_startSyncing_expectSyncProviderStart() { + syncManager.startSyncing(); + verify(mockSyncProvider).start(); + } + + @Test + public void syncManager_stopSyncing_expectSyncProviderStop() { + syncManager.stopSyncing(); + verify(mockSyncProvider).stop(); + } + + @Test + public void syncManager_addEventWithStartAndEndTime_expectSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional fakeTime = Optional.of(LocalDateTime.now()); + + when(mockTask.getStartDateTime()).thenReturn(fakeTime); + when(mockTask.getEndDateTime()).thenReturn(fakeTime); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithStartTime_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional fakeTime = Optional.of(LocalDateTime.now()); + Optional empty = Optional.empty(); + + when(mockTask.getStartDateTime()).thenReturn(empty); + when(mockTask.getEndDateTime()).thenReturn(fakeTime); + + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithEndTime_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional fakeTime = Optional.of(LocalDateTime.now()); + Optional empty = Optional.empty(); + + when(mockTask.getStartDateTime()).thenReturn(fakeTime); + when(mockTask.getEndDateTime()).thenReturn(empty); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithNoTime_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional empty = Optional.empty(); + + when(mockTask.getStartDateTime()).thenReturn(empty); + when(mockTask.getEndDateTime()).thenReturn(empty); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithSyncManagerNotRunning_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_deleteEventWithSyncManagerRunning_expectSyncProviderDelete() { + Task mockTask = mock(Task.class); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.deleteEvent(mockTask); + + verify(mockSyncProvider).deleteEvent(mockTask); + } + + @Test + public void syncManager_deleteEventWithSyncManagerNotRunning_expectNoSyncProviderDelete() { + Task mockTask = mock(Task.class); + + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + syncManager.deleteEvent(mockTask); + + verify(mockSyncProvider, never()).deleteEvent(mockTask); + } +} +``` +###### /java/seedu/agendum/sync/SyncProviderGoogleTests.java +``` java +public class SyncProviderGoogleTests { + private static final File DATA_STORE_CREDENTIAL = new File(DEFAULT_DATA_DIR + "StoredCredential"); + + private static final List DATA_STORE_TEST_CREDENTIALS = Arrays.asList( + new File("cal/StoredCredential_1"), + new File("cal/StoredCredential_2"), + new File("cal/StoredCredential_3") + ); + + private static final SyncProviderGoogle syncProviderGoogle = spy(new SyncProviderGoogle()); + private static final SyncManager mockSyncManager = mock(SyncManager.class); + private static final Task mockTask = mock(Task.class); + + @BeforeClass + public static void setUp() { + copyTestCredentials(); + + try { + Optional fakeTime = Optional.of(LocalDateTime.now()); + Name fakeName = new Name("AGENDUMTESTENGINE"); + int minId = 99999; + int maxId = 9999999; + Random r = new Random(); + + when(mockTask.getStartDateTime()).thenReturn(fakeTime); + when(mockTask.getEndDateTime()).thenReturn(fakeTime); + when(mockTask.getName()).thenReturn(fakeName); + when(mockTask.syncCode()).thenReturn(r.nextInt((maxId - minId) + 1) + minId); + } catch (IllegalValueException e) { + e.printStackTrace(); + } + + syncProviderGoogle.setManager(mockSyncManager); + syncProviderGoogle.start(); + } + + @AfterClass + public static void tearDown() { + deleteCredential(); + } + + public static void copyTestCredentials() { + try { + deleteCredential(); + Files.copy(getRandomCredential().toPath(), DATA_STORE_CREDENTIAL.toPath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void deleteCredential() { + DATA_STORE_CREDENTIAL.delete(); + } + + private static File getRandomCredential() { + int r = new Random().nextInt(DATA_STORE_TEST_CREDENTIALS.size()); + return DATA_STORE_TEST_CREDENTIALS.get(r); + } + + @Test + public void syncProviderGoogle_start_createCalendar() { + reset(syncProviderGoogle); + syncProviderGoogle.deleteAgendumCalendar(); + syncProviderGoogle.start(); + + // Verify if Sync Manager's status was changed + verify(mockSyncManager, atLeastOnce()).setSyncStatus(Sync.SyncStatus.RUNNING); + } + + @Test + public void syncProviderGoogle_startIfNeeded_credentialsFound() { + reset(syncProviderGoogle); + syncProviderGoogle.startIfNeeded(); + + // Verify Sync Provider did start + verify(syncProviderGoogle).start(); + } + + @Test + public void syncProviderGoogle_startIfNeeded_credentialsNotFound() { + reset(syncProviderGoogle); + deleteCredential(); + syncProviderGoogle.startIfNeeded(); + + // Verify Sync Provider should not start + verify(syncProviderGoogle, never()).start(); + } + + @Test + public void syncProviderGoogle_stop_successful() { + reset(mockSyncManager); + syncProviderGoogle.stop(); + + // Verify sync status changed + verify(mockSyncManager).setSyncStatus(Sync.SyncStatus.NOTRUNNING); + assertFalse(DATA_STORE_CREDENTIAL.exists()); + } + + @Test + public void syncProviderGoogle_addEvent_successful() { + syncProviderGoogle.addNewEvent(mockTask); + } + + @Test + public void syncProviderGoogle_deleteEvent_successful() { + syncProviderGoogle.deleteEvent(mockTask); + } + +} +``` diff --git a/collated/test/A0133367E.md b/collated/test/A0133367E.md new file mode 100644 index 000000000000..2981baf45cd5 --- /dev/null +++ b/collated/test/A0133367E.md @@ -0,0 +1,1076 @@ +# A0133367E +###### /java/guitests/AddCommandTest.java +``` java +public class AddCommandTest extends ToDoListGuiTest { + + @Test + public void add() throws IllegalValueException { + //add one task + TestTask[] currentList = td.getTypicalTasks(); + TestTask taskToAdd = TypicalTestTasks.getFloatingTestTask(); + assertAddSuccess(taskToAdd, currentList); + currentList = TestUtil.addTasksToList(currentList, taskToAdd); + + //add another deadline task + taskToAdd = TypicalTestTasks.getDeadlineTestTask(); + assertAddSuccess(taskToAdd, currentList); + currentList = TestUtil.addTasksToList(currentList, taskToAdd); + + //add another event task + taskToAdd = TypicalTestTasks.getEventTestTask(); + assertAddSuccess(taskToAdd, currentList); + currentList = TestUtil.addTasksToList(currentList, taskToAdd); + + //add duplicate task + commandBox.runCommand(TypicalTestTasks.getFloatingTestTask().getAddCommand()); + assertResultMessage(Messages.MESSAGE_DUPLICATE_TASK); + assertAllPanelsMatch(currentList); + + //add to empty list + commandBox.runCommand("delete 1-10"); + assertAddSuccess(TypicalTestTasks.getFloatingTestTask()); + + //invalid command + commandBox.runCommand("adds Johnny"); + assertResultMessage(String.format(Messages.MESSAGE_UNKNOWN_COMMAND_WITH_SUGGESTION, "add")); + } + + private void assertAddSuccess(TestTask taskToAdd, TestTask... currentList) { + commandBox.runCommand(taskToAdd.getAddCommand()); + + //confirm the new card contains the right data + if (taskToAdd.isCompleted()) { + TaskCardHandle addedCard = completedTasksPanel.navigateToTask(taskToAdd.getName().fullName); + assertMatching(taskToAdd, addedCard); + } else if (!taskToAdd.isCompleted() && !taskToAdd.hasTime()) { + TaskCardHandle addedCard = floatingTasksPanel.navigateToTask(taskToAdd.getName().fullName); + assertMatching(taskToAdd, addedCard); + } else if (!taskToAdd.isCompleted() && taskToAdd.hasTime()) { + TaskCardHandle addedCard = upcomingTasksPanel.navigateToTask(taskToAdd.getName().fullName); + assertMatching(taskToAdd, addedCard); + } + + //confirm the list now contains all previous tasks plus the new task + taskToAdd.setLastUpdatedTimeToNow(); + TestTask[] expectedList = TestUtil.addTasksToList(currentList, taskToAdd); + assertAllPanelsMatch(expectedList); + } +} +``` +###### /java/guitests/ToDoListGuiTest.java +``` java + /** + * Asserts the tasks shown in each panel will match + */ + protected void assertAllPanelsMatch(TestTask[] expectedList) { + TestUtil.sortTasks(expectedList); + TestTask[] expectedDoItSoonTasks = TestUtil.getUpcomingTasks(expectedList); + TestTask[] expectedDoItAnytimeTasks = TestUtil.getFloatingTasks(expectedList); + TestTask[] expectedDoneTasks = TestUtil.getCompletedTasks(expectedList); + assertTrue(upcomingTasksPanel.isListMatching(expectedDoItSoonTasks)); + assertTrue(floatingTasksPanel.isListMatching(expectedDoItAnytimeTasks)); + assertTrue(completedTasksPanel.isListMatching(expectedDoneTasks)); + } +} +``` +###### /java/seedu/agendum/logic/LogicManagerTest.java +``` java + /** + * Confirms the 'incorrect index format behaviour' for the given command + * targeting a single task in the shown list, using visible index. + * @param commandWord to test assuming it targets a single task in the last shown list based on visible index. + * @param wordsAfterIndex contains a string that will usually follow the command + * + * This (overloaded) method is created for rename/schedule + */ + private void assertIncorrectIndexFormatBehaviorForCommand(String commandWord, String expectedMessage, String wordsAfterIndex) + throws Exception { + assertCommandBehavior(commandWord + " " + wordsAfterIndex, expectedMessage); //index missing + assertCommandBehavior(commandWord + " +1 " + wordsAfterIndex, expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " -1 " + wordsAfterIndex, expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " 0 " + wordsAfterIndex, expectedMessage); //index cannot be 0 + assertCommandBehavior(commandWord + " not_a_number " + wordsAfterIndex, expectedMessage); + } + + /** + * Confirms the 'incorrect index format behaviour' for the given command + * targeting a single/multiple task(s) in the shown list, using visible indices. + * @param commandWord to test assuming it targets a single/multiple task(s) in the shown list, using visible indices. + * + * This (overloaded) method is created for delete/mark/unmark. + */ + private void assertIncorrectIndexFormatBehaviorForCommand(String commandWord, String expectedMessage) throws Exception { + assertIncorrectIndexFormatBehaviorForCommand(commandWord, expectedMessage, " "); + + // multiple indices + assertCommandBehavior(commandWord + " +1 2 3", expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " 1 2 -3", expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " 1 not_a_number 3 4", expectedMessage); //index cannot be a string + } + + /** + * Confirms the 'invalid argument index number behaviour' for the given command + * targeting a single task in the shown list, using visible index. + * @param commandWord to test assuming it targets a single task in the last shown list based on visible index. + * @param wordsAfterIndex contains a string that will usually follow the command + * + * This (overloaded) method is created for rename/schedule + */ + private void assertIndexNotFoundBehaviorForCommand(String commandWord, String wordsAfterIndex) throws Exception { + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(2); + + // set AB state to 2 tasks + model.resetData(new ToDoList()); + helper.addToModel(model, taskList); + + // test boundary value (one-based index is 3 when list is of size 2) + assertCommandBehavior(commandWord + " 3 " + wordsAfterIndex, MESSAGE_INVALID_TASK_DISPLAYED_INDEX, model.getToDoList(), taskList); + } + + /** + * Confirms the 'invalid argument index number behaviour' for the given command + * targeting a single/multiple task(s) in the shown list, using visible indices. + * @param commandWord to test assuming it targets tasks in the last shown list based on visible indices. + * + * This (overloaded) method is created for delete/mark/unmark. + */ + private void assertIndexNotFoundBehaviorForCommand(String commandWord) throws Exception { + assertIndexNotFoundBehaviorForCommand(commandWord, ""); + + // multiple indices + String expectedMessage = MESSAGE_INVALID_TASK_DISPLAYED_INDEX; + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(5); + + // set AB state to 5 tasks + model.resetData(new ToDoList()); + helper.addToModel(model, taskList); + + // test boundary value (one-based index is 6 when list is of size 5) + //invalid index is the last index given + assertCommandBehavior(commandWord + " 1 6", expectedMessage, model.getToDoList(), taskList); + //invalid index is not the first index + assertCommandBehavior(commandWord + " 1 6 2", expectedMessage, model.getToDoList(), taskList); + //invalid index is part of range + assertCommandBehavior(commandWord + " 1-6", expectedMessage, model.getToDoList(), taskList); + } + + +``` +###### /java/seedu/agendum/logic/LogicManagerTest.java +``` java + @Test + public void execute_delete_removesCorrectSingleTask() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(3); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.removeTask(threeTasks.get(2)); + + // prepare model + helper.addToModel(model, threeTasks); + + // prepare message + List deletedTaskVisibleIndices = helper.generateNumberList(3); + List deletedTasks = helper.generateReadOnlyTaskList(threeTasks.get(2)); + String tasksAsString = CommandResult.tasksToString(deletedTasks, deletedTaskVisibleIndices); + + // test boundary value (last task in the list) + assertCommandBehavior("delete 3", + String.format(DeleteCommand.MESSAGE_DELETE_TASK_SUCCESS, tasksAsString), + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_delete_removesCorrectRangeOfTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.removeTask(fourTasks.get(2)); + expectedTDL.removeTask(fourTasks.get(1)); + + // prepare model + helper.addToModel(model, fourTasks); + + //prepare message + List deletedTaskVisibleIndices = helper.generateNumberList(2, 3); + List deletedTasks = helper.generateReadOnlyTaskList(fourTasks.get(1), fourTasks.get(2)); + String tasksAsString = CommandResult.tasksToString(deletedTasks, deletedTaskVisibleIndices); + + // Delete tasks with visible index in range [startIndex, endIndex] = [2, 3] + // Checks if the new to do list contains Task 1 and Task 4 from the last visible list + assertCommandBehavior("delete 2-3", + String.format(DeleteCommand.MESSAGE_DELETE_TASK_SUCCESS, tasksAsString), + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_delete_removesCorrectMultipleTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.removeTask(fourTasks.get(3)); + expectedTDL.removeTask(fourTasks.get(2)); + expectedTDL.removeTask(fourTasks.get(1)); + + // prepare model + helper.addToModel(model, fourTasks); + + // prepare message + List deletedTaskVisibleIndices = helper.generateNumberList(2, 3, 4); + List deletedTasks = helper.generateReadOnlyTaskList( + fourTasks.get(1), fourTasks.get(2), fourTasks.get(3)); + String tasksAsString = CommandResult.tasksToString(deletedTasks, deletedTaskVisibleIndices); + + assertCommandBehavior("delete 2,3 4", + String.format(DeleteCommand.MESSAGE_DELETE_TASK_SUCCESS, tasksAsString), + expectedTDL, + expectedTDL.getTaskList()); + } + //@author + + @Test + public void execute_syncOn_successfull() throws Exception { + SyncProviderGoogleTests.copyTestCredentials(); + assertCommandBehavior("sync on", + SyncCommand.SYNC_ON_MESSAGE); + + SyncProviderGoogleTests.deleteCredential(); // Clean up credential file + } + + @Test + public void execute_syncOff_successfull() throws Exception { + assertCommandBehavior("sync off", + SyncCommand.SYNC_OFF_MESSAGE); + } + + @Test + public void execute_syncUnknown_exception() throws Exception { + assertCommandBehavior("sync something", SyncCommand.MESSAGE_WRONG_OPTION, new ToDoList(), Collections.emptyList()); + } + + +``` +###### /java/seedu/agendum/logic/LogicManagerTest.java +``` java + @Test + public void execute_markInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("mark", expectedMessage); + } + + @Test + public void execute_markIndexNotFound_errorMessageShown() throws Exception { + assertIndexNotFoundBehaviorForCommand("mark"); + } + + @Test + public void execute_markToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List tasks = helper.generateTaskList(4); + tasks.add(helper.generateCompletedTask(2)); + + ToDoList expectedTDL = helper.generateToDoList(tasks); + + helper.addToModel(model, tasks); + + assertCommandBehavior("mark 1-3", + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_mark_marksCorrectSingleTaskAsCompleted() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(3); + + // prepared expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.markTask(threeTasks.get(0)); + + // prepare model + helper.addToModel(model, threeTasks); + + // test boundary value (first task in the list) + assertCommandBehavior("mark 1", + MarkCommand.MESSAGE_MARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_mark_marksCorrectRangeOfTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.markTask(fourTasks.get(2)); + expectedTDL.markTask(fourTasks.get(3)); + + // prepare model + helper.addToModel(model, fourTasks); + + // test boundary value (up to last task in the list) + assertCommandBehavior("mark 3-4", + MarkCommand.MESSAGE_MARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_mark_marksCorrectMultipleTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.markTask(fourTasks.get(1)); + expectedTDL.markTask(fourTasks.get(2)); + expectedTDL.markTask(fourTasks.get(3)); + + // prepare model + helper.addToModel(model, fourTasks); + + assertCommandBehavior("mark 2,3 4", + MarkCommand.MESSAGE_MARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_unmarkInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnmarkCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("unmark", expectedMessage); + } + + @Test + public void execute_unmarkIndexNotFound_errorMessageShown() throws Exception { + assertIndexNotFoundBehaviorForCommand("unmark"); + } + + @Test + public void execute_unmarkToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List tasks = helper.generateCompletedTaskList(4); + tasks.add(helper.generateTask(2)); + + ToDoList expectedTDL = helper.generateToDoList(tasks); + + helper.addToModel(model, tasks); + + assertCommandBehavior("unmark 3-5", + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_unmark_UnmarksCorrectSingleTaskFromCompleted() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(2); + threeTasks.add(helper.generateCompletedTask(3)); + + // prepare expectedTDL - does not have any tasks marked as completed + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.unmarkTask(threeTasks.get(2)); + + // prepare model + helper.addToModel(model, threeTasks); + + // test boundary value - last task in the list + assertCommandBehavior("unmark 3", + UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_unmark_unmarksCorrectRangeOfTasks() throws Exception { + // indexes provided are startIndex-endIndex. + // Tasks with visible index in range [startIndex, endIndex] are marked + TestDataHelper helper = new TestDataHelper(); + List fourCompletedTasks = helper.generateCompletedTaskList(4); + List fourTasks = helper.generateTaskList(4); + + // prepare expectedTDL - does not have any tasks marked as completed + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + + // prepare model + helper.addToModel(model, fourCompletedTasks); + + assertCommandBehavior("unmark 1-4", + UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_unmark_unmarksCorrectMultipleTasks() throws Exception { + // unmark multiple indices specified (separated by space/comma) + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateCompletedTaskList(4); + + // prepare expectedTDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.unmarkTask(fourTasks.get(3)); + expectedTDL.unmarkTask(fourTasks.get(2)); + expectedTDL.unmarkTask(fourTasks.get(1)); + + // prepare model + helper.addToModel(model, fourTasks); + + assertCommandBehavior("unmark 2,3 4", + String.format(UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS), + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_renameInvalidArgsFormat_errorMessageShown() throws Exception { + // invalid index format + // a valid name is provided since invalid input values must be tested one at a time + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, RenameCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("rename", expectedMessage, " new task name"); + + // invalid new task name format e.g. task name is not provided + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(2); + + helper.addToModel(model, taskList); + + // a valid index is provided since we are testing for invalid name (empty string) here + assertCommandBehavior("rename 1 ", expectedMessage, model.getToDoList(), taskList); + + } + + @Test + public void execute_renameIndexNotFound_errorMessageShown() throws Exception { + // a valid name is provided to only test for invalid index + assertIndexNotFoundBehaviorForCommand("rename", " new task name"); + } + + @Test + public void execute_renameToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task toBeDuplicated = helper.adam(); + Task toBeRenamed = helper.generateTask(1); + List twoTasks = helper.generateTaskList(toBeDuplicated, toBeRenamed); + ToDoList expectedTDL = helper.generateToDoList(twoTasks); + + helper.addToModel(model, twoTasks); + + // execute command and verify result + // a valid index must be provided to check if the name is invalid (due to a duplicate) + assertCommandBehavior( + "rename 2 " + toBeDuplicated.getName().toString(), + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_rename_RenamesCorrectTask() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(2); + Task taskToRename = helper.generateCompletedTask(3); + threeTasks.add(taskToRename); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + Task renamedTask = new Task(taskToRename); + String newTaskName = "a brand new task name"; + renamedTask.setName(new Name(newTaskName)); + expectedTDL.updateTask(taskToRename, renamedTask); + + // prepare model + helper.addToModel(model, threeTasks); + + // boundary value: use the last task + assertCommandBehavior("rename 3 " + newTaskName, + String.format(RenameCommand.MESSAGE_SUCCESS, newTaskName), + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_scheduleInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE); + + // no index and time are provided + assertCommandBehavior("schedule", expectedMessage, new ToDoList(), Collections.emptyList()); + + // invalid index format + // a valid time is provided since invalid input values must be tested one at a time + assertIncorrectIndexFormatBehaviorForCommand("schedule", expectedMessage, "by 9pm"); + + // invalid time format provided + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(2); + helper.addToModel(model, taskList); + + // a valid index is provided since we are testing for invalid time format here + assertCommandBehavior("schedule 1 blue", expectedMessage, model.getToDoList(), taskList); + assertCommandBehavior("schedule 1 from 7pm", expectedMessage, model.getToDoList(), taskList); + } + + @Test + public void execute_scheduleIndexNotFound_errorMessageShown() throws Exception { + // a valid time is provided to only test for invalid index + assertIndexNotFoundBehaviorForCommand("schedule", "by 9pm"); + } + + @Test + public void execute_scheduleToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task toBeDuplicated = helper.generateTask(1); + LocalDateTime time = LocalDateTime.of(2016, 10, 10, 10, 10); + toBeDuplicated.setEndDateTime(Optional.ofNullable(time)); + Task toBeScheduled = helper.generateTask(1); + List twoTasks = helper.generateTaskList(toBeDuplicated, toBeScheduled); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(twoTasks); + + // prepare model + model.resetData(expectedTDL); + + // execute command and verify result + // a valid index must be provided to check if the time is invalid (due to a duplicate) + assertCommandBehavior( + "schedule 2 by Oct 10 10:10", + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_schedule_scheduleCorrectTask() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(2); + + Task floatingTask = helper.generateTask(3); + threeTasks.add(floatingTask); + + LocalDateTime endTime = LocalDateTime.of(2016, 10, 10, 10, 10); + LocalDateTime startTime = LocalDateTime.of(2016, 9, 9, 9, 10); + Task eventTask = helper.generateTask(3); + eventTask.setStartDateTime(Optional.ofNullable(startTime)); + eventTask.setEndDateTime(Optional.ofNullable(endTime)); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.updateTask(floatingTask, eventTask); + + // prepare model + helper.addToModel(model, threeTasks); + + assertCommandBehavior("schedule 3 from Sep 9 9:10 to Oct 10 10:10", + String.format(ScheduleCommand.MESSAGE_SUCCESS, eventTask), + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_aliasInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AliasCommand.MESSAGE_USAGE); + assertCommandBehavior("alias", expectedMessage, new ToDoList(), Collections.emptyList()); + // alias should not contain symbols + assertCommandBehavior("alias add +", expectedMessage, new ToDoList(), Collections.emptyList()); + // new alias key has space + assertCommandBehavior("alias add a 1", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasNonOriginalCommand_errorMessageShown() throws Exception { + String expectedMessage = String.format(AliasCommand.MESSAGE_FAILURE_NON_ORIGINAL_COMMAND, "a"); + assertCommandBehavior("alias a short", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasKeyIsReservedCommandWord_errorMessageShown() throws Exception { + String expectedMessage = String.format(AliasCommand.MESSAGE_FAILURE_RESERVED_COMMAND_WORD, + RenameCommand.COMMAND_WORD); + String userCommand = "alias " + AddCommand.COMMAND_WORD + " " + RenameCommand.COMMAND_WORD; + assertCommandBehavior(userCommand, expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasKeyIsInUse_errorMessageShown() throws Exception { + String expectedMessage = String.format(AliasCommand.MESSAGE_FAILURE_ALIAS_IN_USE, + "r", RenameCommand.COMMAND_WORD); + CommandLibrary.getInstance().addNewAlias("r", RenameCommand.COMMAND_WORD); + String userCommand = "alias " + AddCommand.COMMAND_WORD + " r"; + assertCommandBehavior(userCommand, expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasValidCaseInsensitiveKey_successfullyAdded() throws Exception { + // successfully add alias + String expectedMessage = String.format(AliasCommand.MESSAGE_SUCCESS, "a", AddCommand.COMMAND_WORD); + + assertCommandBehavior("alias " + AddCommand.COMMAND_WORD + " A", + expectedMessage, new ToDoList(), Collections.emptyList()); + + // alias can be used + TestDataHelper helper = new TestDataHelper(); + + Task addedTask = helper.generateTaskWithName("new task"); + List taskList = helper.generateTaskList(addedTask); + + ToDoList expectedTDL = helper.generateToDoList(taskList); + expectedMessage = String.format(AddCommand.MESSAGE_SUCCESS, addedTask); + + assertCommandBehavior("a new task", expectedMessage, expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_unaliasInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnaliasCommand.MESSAGE_USAGE); + assertCommandBehavior("unalias", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_unaliasReservedCommandWord_errorMessageShown() throws Exception { + String expectedMessage = String.format(UnaliasCommand.MESSAGE_FAILURE_RESERVED_COMMAND_WORD, + AddCommand.COMMAND_WORD); + String command = "unalias " + AddCommand.COMMAND_WORD; + assertCommandBehavior(command, expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_unaliasNoSuchKey_errorMessageShown() throws Exception { + String expectedMessage = String.format(UnaliasCommand.MESSAGE_FAILURE_NO_ALIAS_KEY, "smth"); + assertCommandBehavior("unalias smth", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_unaliasExistingAliasKey_successfullyRemoved() throws Exception { + // successfully remove alias + String expectedMessage = String.format(UnaliasCommand.MESSAGE_SUCCESS, "wow"); + CommandLibrary.getInstance().addNewAlias("wow", AddCommand.COMMAND_WORD); + assertCommandBehavior("unalias wow", expectedMessage, new ToDoList(), Collections.emptyList()); + + // previous alias key cannot be used + expectedMessage = MESSAGE_UNKNOWN_COMMAND; + assertCommandBehavior("wow new task", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + +``` +###### /java/seedu/agendum/logic/LogicManagerTest.java +``` java + @Test + public void execute_undo_identifiesNoPreviousChanges() throws Exception { + assertCommandBehavior("undo", UndoCommand.MESSAGE_FAILURE, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_undo_reversePreviousChangeToTaskList() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task p1 = helper.generateTaskWithName("old name"); + List listWithOneTask = helper.generateTaskList(p1); + ToDoList expectedTDL = helper.generateToDoList(listWithOneTask); + List readOnlyTaskList = helper.generateReadOnlyTaskList(p1); + + // Undo add command + model.addTask(p1); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, new ToDoList(), Collections.emptyList()); + + // Undo delete command + model.addTask(p1); + model.deleteTasks(readOnlyTaskList); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, expectedTDL, listWithOneTask); + + // Undo rename command + Task p2 = new Task(p1); + p2.setName(new Name("new name")); + model.updateTask(p1, p2); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, expectedTDL, listWithOneTask); + + // Undo mark command + model.markTasks(readOnlyTaskList); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, expectedTDL, listWithOneTask); + } + + +``` +###### /java/seedu/agendum/model/TaskTest.java +``` java +public class TaskTest { + + private Task floatingTask; + private Task eventTask; + private Task deadlineTaskDueYesterday; + private Task deadlineTaskDueTomorrow; + private Optional noTimeSpecified = Optional.empty(); + private Optional yesterday = Optional.ofNullable(LocalDateTime.now().minusDays(1)); + private Optional tomorrow = Optional.ofNullable(LocalDateTime.now().plusDays(1)); + + /** + * Convenient helper method to generate a task with Name name, + * the specified completion status, start and end time (specified by optional). + * Last updated time is set to yesterday. + */ + private Task generateTask(String name, boolean isCompleted, Optional startTime, + Optional endTime) throws Exception { + Name taskName = new Name(name); + Task task = new Task(taskName, startTime, endTime); + if (isCompleted) { + task.markAsCompleted(); + } + task.setLastUpdatedTime(yesterday.get()); + return task; + } + + @Before + public void setUp() throws Exception { + floatingTask = generateTask("task", false, noTimeSpecified, noTimeSpecified); + eventTask = generateTask("task", false, yesterday, tomorrow); + deadlineTaskDueYesterday = generateTask("task", false, noTimeSpecified, yesterday); + deadlineTaskDueTomorrow = generateTask("task", false, noTimeSpecified, tomorrow); + } + + @Test + public void isOverdue_floatingTask_returnsFalse() { + assertFalse(floatingTask.isOverdue()); + } + + @Test + public void isOverdue_completedTask_returnsFalse() { + // testing for completion status, give valid (overdue) end date time + deadlineTaskDueYesterday.markAsCompleted(); + assertFalse(deadlineTaskDueYesterday.isOverdue()); + } + + @Test + public void isOverdue_uncompletedFutureTask_returnsFalse() { + assertFalse(deadlineTaskDueTomorrow.isOverdue()); + } + + @Test + public void isOverdue_uncompletedTaskFromYesterday_returnsTrue() { + assertTrue(deadlineTaskDueYesterday.isOverdue()); + assertTrue(eventTask.isOverdue()); + } + + @Test + public void isUpcoming_floatingTask_returnsFalse() { + assertFalse(floatingTask.isUpcoming()); + } + + @Test + public void isUpcoming_completedTask_returnsFalse() { + // testing for completion status, give valid (upcoming) end date time + deadlineTaskDueTomorrow.markAsCompleted(); + assertFalse(deadlineTaskDueTomorrow.isUpcoming()); + } + + @Test + public void isUpcoming_taskWithEndTimeYesterday_returnsFalse() { + assertFalse(deadlineTaskDueYesterday.isUpcoming()); + } + + @Test + public void isUpcoming_uncompletedTaskFromNextMonth_returnsFalse() { + LocalDateTime nextMonth = LocalDateTime.now().plusMonths(1); + floatingTask.setEndDateTime(Optional.ofNullable(nextMonth)); + assertFalse(floatingTask.isUpcoming()); + } + + @Test + public void isUpcoming_uncompletedTaskFromTomorrow_returnsTrue() { + assertTrue(deadlineTaskDueTomorrow.isUpcoming()); + } + + @Test + public void isEvent_floatingTask_returnsFalse() { + assertFalse(floatingTask.isEvent()); + } + + @Test + public void isEvent_taskWithNoStartTime_returnsFalse() { + assertFalse(deadlineTaskDueTomorrow.isEvent()); + } + + @Test + public void isEvent_taskHasStartAndEndTime_returnsTrue() { + assertTrue(eventTask.isEvent()); + } + + @Test + public void hasDeadline_floatingTask_returnsFalse() { + assertFalse(floatingTask.hasDeadline()); + } + + @Test + public void hasDeadline_taskWithEndTimeButNoStartTime_returnsTrue() { + assertTrue(deadlineTaskDueTomorrow.hasDeadline()); + } + + @Test + public void hasDeadline_taskHasStartAndEndTime_returnsFalse() { + assertFalse(eventTask.hasDeadline()); + } + + @Test + public void setName_updateNameAndPreserveProperties() throws Exception { + Task copiedTask = new Task(eventTask); + eventTask.setName(new Name("updated task")); + + assertEquals(eventTask.getName().toString(), "updated task"); + assertEquals(eventTask.getStartDateTime(), copiedTask.getStartDateTime()); + assertEquals(eventTask.getEndDateTime(), copiedTask.getEndDateTime()); + assertEquals(eventTask.isCompleted(), copiedTask.isCompleted()); + // should change the last updated time + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void setStartTime_updateStartTimeAndPreserveProperties() { + Task copiedTask = new Task(eventTask); + eventTask.setStartDateTime(Optional.empty()); + + assertEquals(eventTask.getStartDateTime(), Optional.empty()); + assertEquals(eventTask.getName(), copiedTask.getName()); + assertEquals(eventTask.getEndDateTime(), copiedTask.getEndDateTime()); + assertEquals(eventTask.isCompleted(), copiedTask.isCompleted()); + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void setEndTime_updateEndTimeAndPreserveProperties() { + Task copiedTask = new Task(eventTask); + eventTask.setEndDateTime(yesterday); + + assertEquals(eventTask.getEndDateTime(), yesterday); + assertEquals(eventTask.getName(), copiedTask.getName()); + assertEquals(eventTask.getStartDateTime(), copiedTask.getStartDateTime()); + assertEquals(eventTask.isCompleted(), copiedTask.isCompleted()); + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void markAsCompleted_markAsCompletedAndPreserveProperties() { + Task copiedTask = new Task(eventTask); + eventTask.markAsCompleted(); + + assertTrue(eventTask.isCompleted()); + assertEquals(eventTask.getName(), copiedTask.getName()); + assertEquals(eventTask.getStartDateTime(), copiedTask.getStartDateTime()); + assertEquals(eventTask.getEndDateTime(), copiedTask.getEndDateTime()); + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void equals_tasksWithOnlyDifferentName_returnsFalse() throws Exception { + Task anotherTask = generateTask("new task", false, noTimeSpecified, noTimeSpecified); + assertFalse(floatingTask.equals(anotherTask)); + } + + @Test + public void equals_tasksWithOnlyDifferentCompletionStatus_returnsFalse() throws Exception { + Task anotherTask = generateTask("task", true, noTimeSpecified, noTimeSpecified); + assertFalse(floatingTask.equals(anotherTask)); + } + + @Test + public void equals_tasksWithOnlyDifferentTaskTime_returnsFalse() throws Exception { + assertFalse(deadlineTaskDueTomorrow.equals(deadlineTaskDueYesterday)); + } + + @Test + public void equals_tasksWithOnlyDifferentUpdatedTime_returnsTrue() throws Exception { + Task copiedTask = new Task(floatingTask); + copiedTask.setLastUpdatedTimeToNow(); + assertEquals(floatingTask, copiedTask); + } + + @Test + public void compareTo_uncompletedAndCompletedTasks_uncompletedFirst() throws Exception { + // tasks with same time + Task completedTask = generateTask("task", true, noTimeSpecified, noTimeSpecified); + assertTrue(floatingTask.compareTo(completedTask) < 0); + + // tasks with different end time and updated time + deadlineTaskDueYesterday.markAsCompleted(); + assertTrue(floatingTask.compareTo(deadlineTaskDueYesterday) < 0); + } + + /** + * Compare uncompleted tasks based on their task time (start time if present, else end time) + */ + @Test + public void compareTo_uncompletedTasks_earlierAndPresentTaskTimeFirst() { + assertTrue(deadlineTaskDueYesterday.compareTo(deadlineTaskDueTomorrow) < 0); + assertTrue(deadlineTaskDueTomorrow.compareTo(floatingTask) < 0); + assertTrue(floatingTask.compareTo(deadlineTaskDueYesterday) > 0); + } + + /** + * Compare completed tasks based on their last updated time + * (Regardless of the start and end time associated with the task) + */ + @Test + public void compareTo_completedTasks_laterUpdatedTimeFirst() { + + deadlineTaskDueTomorrow.markAsCompleted(); + deadlineTaskDueTomorrow.setLastUpdatedTime(yesterday.get()); + + // have later updated time + deadlineTaskDueYesterday.markAsCompleted(); + deadlineTaskDueYesterday.setLastUpdatedTime(tomorrow.get()); + + assertTrue(deadlineTaskDueYesterday.compareTo(deadlineTaskDueTomorrow) < 0); + } + + @Test + public void compareTo_tasksWithOnlyDifferentNames_lexicographicalOrder() throws Exception { + Task anotherTask = generateTask("another task", false, noTimeSpecified, noTimeSpecified); + assertTrue(anotherTask.compareTo(floatingTask) < 0); + } + +} +``` +###### /java/seedu/agendum/model/UniqueTaskListTest.java +``` java +public class UniqueTaskListTest { + + private UniqueTaskList uniqueTaskList; + private ObservableList internalList; + + private Task originalTask; + private Task duplicateOfOriginalTask; + private Task newTask; + + @Before + public void setUp() throws IllegalValueException { + uniqueTaskList = new UniqueTaskList(); + + originalTask = new Task(new Name("task")); + duplicateOfOriginalTask = new Task(originalTask); + newTask = new Task(new Name("new task")); + + uniqueTaskList.add(originalTask); + + internalList = FXCollections.observableArrayList(); + internalList.add(originalTask); + } + + @Test + public void contains_taskInEmptyList_returnsFalse() { + uniqueTaskList = new UniqueTaskList(); + assertFalse(uniqueTaskList.contains(originalTask)); + } + + @Test + public void contains_taskWithDifferentState_returnsFalse() throws Exception { + assertFalse(uniqueTaskList.contains(newTask)); + } + + @Test + public void contains_taskWithSameState_returnsTrue() throws Exception { + assertTrue(uniqueTaskList.contains(duplicateOfOriginalTask)); + } + + @Test(expected = DuplicateTaskException.class) + public void add_duplicateTask_throwsException() throws Exception { + uniqueTaskList.add(duplicateOfOriginalTask); + } + + @Test + public void add_newTask_successful() throws Exception { + uniqueTaskList.add(newTask); + internalList.add(newTask); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = TaskNotFoundException.class) + public void remove_absentTask_throwsException() throws Exception { + uniqueTaskList.remove(newTask); + } + + @Test + public void remove_existingTask_successful() throws Exception { + uniqueTaskList.remove(duplicateOfOriginalTask); + internalList.clear(); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = DuplicateTaskException.class) + public void update_duplicateTask_throwsException() throws Exception { + uniqueTaskList.add(newTask); + uniqueTaskList.update(newTask, duplicateOfOriginalTask); + } + + @Test(expected = TaskNotFoundException.class) + public void update_absentTask_throwsException() throws Exception { + uniqueTaskList.update(newTask, newTask); + } + + @Test + public void update_existingTask_successful() throws Exception { + uniqueTaskList.update(originalTask, newTask); + internalList.set(0, newTask); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = DuplicateTaskException.class) + public void mark_resultInDuplicateTask_throwsException() throws Exception { + duplicateOfOriginalTask.markAsCompleted(); + uniqueTaskList.add(duplicateOfOriginalTask); + uniqueTaskList.mark(originalTask); + } + + @Test(expected = TaskNotFoundException.class) + public void mark_absentTask_throwsException() throws Exception { + uniqueTaskList.mark(newTask); + } + + @Test + public void mark_existingTask_successful() throws Exception { + uniqueTaskList.mark(originalTask); + internalList.get(0).markAsCompleted(); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = DuplicateTaskException.class) + public void unmark_resultInDuplicateTask_throwsException() throws Exception { + // cannot unmark a task that have not been mark as completed + uniqueTaskList.unmark(originalTask); + } + + @Test(expected = TaskNotFoundException.class) + public void unmark_absentTask_throwsException() throws Exception { + uniqueTaskList.unmark(newTask); + } + + @Test + public void unmark_existingTask_successful() throws Exception { + uniqueTaskList = new UniqueTaskList(); + duplicateOfOriginalTask.markAsCompleted(); + uniqueTaskList.add(duplicateOfOriginalTask); + uniqueTaskList.unmark(duplicateOfOriginalTask); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } +} +``` diff --git a/collated/test/A0148031R.md b/collated/test/A0148031R.md new file mode 100644 index 000000000000..45c596da1c9f --- /dev/null +++ b/collated/test/A0148031R.md @@ -0,0 +1,615 @@ +# A0148031R +###### /java/guitests/CommandBoxTest.java +``` java +public class CommandBoxTest extends ToDoListGuiTest { + + @Test + public void commandBox_CommandSucceeds_TextCleared() throws IllegalValueException { + commandBox.runCommand(TypicalTestTasks.BENSON.getAddCommand()); + assertEquals(commandBox.getCommandInput(), ""); + } + + @Test + public void commandBox_CommandFails_TextStays(){ + commandBox.runCommand("invalid command"); + assertEquals(commandBox.getCommandInput(), "invalid command"); + //TODO: confirm the text box color turns to red + } + + @Test + public void commandBox_CommandHistory_Empty() { + // No previous command + commandBox.scrollToPreviousCommand(); + assertEquals(commandBox.getCommandInput(), ""); + + // No next command + commandBox.scrollToNextCommand(); + assertEquals(commandBox.getCommandInput(), ""); + } + + @Test + public void commandBox_CommandHistory_Exists() { + String addCommand = "add commandhistorytestevent"; + commandBox.runCommand(addCommand); + commandBox.runCommand("undo"); + + // Get previous undo command + commandBox.scrollToPreviousCommand(); + assertEquals(commandBox.getCommandInput(), "undo"); + + // Get previous add command + commandBox.scrollToPreviousCommand(); + assertEquals(commandBox.getCommandInput(), addCommand); + + // Get next undo command + commandBox.scrollToNextCommand(); + assertEquals(commandBox.getCommandInput(), "undo"); + + // No next command + commandBox.scrollToNextCommand(); + assertEquals(commandBox.getCommandInput(), ""); + } + +} +``` +###### /java/guitests/FindCommandTest.java +``` java + @Test + public void find_showMesssage() { + commandBox.runCommand("find Meier"); + assertShowingMessage(Messages.MESSAGE_ESCAPE_HELP_WINDOW); + assertFindResult("find Meier", TypicalTestTasks.BENSON, TypicalTestTasks.DANIEL); + } + + @Test + public void find_showMessage_fail() { + commandBox.runCommand("find2"); + assertShowingMessage(null); + } + + @Test + public void find_backToAllTasks_WithEscape() { + assertFindResult("find Meier", TypicalTestTasks.BENSON, TypicalTestTasks.DANIEL); + assertShowingMessage(Messages.MESSAGE_ESCAPE_HELP_WINDOW); + mainGui.pressEscape(); + assertAllPanelsMatch(td.getTypicalTasks()); + } + +``` +###### /java/guitests/guihandles/CommandBoxHandle.java +``` java +/** + * A handle to the Command Box in the GUI. + */ +public class CommandBoxHandle extends GuiHandle{ + + private static final String COMMAND_INPUT_FIELD_ID = "#commandTextField"; + + public CommandBoxHandle(GuiRobot guiRobot, Stage primaryStage, String stageTitle) { + super(guiRobot, primaryStage, stageTitle); + } + + public void enterCommand(String command) { + setTextField(COMMAND_INPUT_FIELD_ID, command); + } + + public String getCommandInput() { + return getTextFieldText(COMMAND_INPUT_FIELD_ID); + } + + /** + * Enters the given command in the Command Box and presses enter. + */ + public void runCommand(String command) { + enterCommand(command); + pressEnter(); + + //Give time for the command to take effect + guiRobot.sleep(2000); + } + + public HelpWindowHandle runHelpCommand() { + enterCommand("help"); + pressEnter(); + return new HelpWindowHandle(guiRobot, primaryStage); + } + + public void scrollToPreviousCommand() { + guiRobot.push(new KeyCodeCombination(KeyCode.UP)); + guiRobot.sleep(200); + } + + public void scrollToNextCommand() { + guiRobot.push(new KeyCodeCombination(KeyCode.DOWN)); + guiRobot.sleep(200); + } + +} +``` +###### /java/guitests/guihandles/HelpWindowHandle.java +``` java +/** + * Provides a handle to the help window of the app. + */ +public class HelpWindowHandle extends GuiHandle { + + private static final String HELP_WINDOW_TITLE = "Help"; + private static final String HELP_WINDOW_ROOT_FIELD_ID = "#helpWindowRoot"; + + public HelpWindowHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, HELP_WINDOW_TITLE); + guiRobot.sleep(1000); + } + + public boolean isWindowOpen() { + return getNode(HELP_WINDOW_ROOT_FIELD_ID) != null + && getNode(HELP_WINDOW_ROOT_FIELD_ID).getParent() != null; + } + + public boolean isWindowClose() { + try { + getNode(HELP_WINDOW_ROOT_FIELD_ID); + } catch (IllegalStateException e) { + return true; + } + return false; + } + + public void closeWindow() { + super.pressEscape(); + guiRobot.sleep(500); + } + +} +``` +###### /java/guitests/guihandles/MainGuiHandle.java +``` java +/** + * Provides a handle for the main GUI. + */ +public class MainGuiHandle extends GuiHandle { + + public MainGuiHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public UpcomingTasksHandle getDoItSoonPanel() { + return new UpcomingTasksHandle(guiRobot, primaryStage); + } + + public FloatingTasksPanelHandle getDoItAnytimePanel() { + return new FloatingTasksPanelHandle(guiRobot, primaryStage); + } + + public CompletedTasksPanelHandle getCompletedTasksPanel() { + return new CompletedTasksPanelHandle(guiRobot, primaryStage); + } + + public ResultDisplayHandle getResultDisplay() { + return new ResultDisplayHandle(guiRobot, primaryStage); + } + + public MessageDisplayHandle getMessageDisplay() { + return new MessageDisplayHandle(guiRobot, primaryStage); + } + + public CommandBoxHandle getCommandBox() { + return new CommandBoxHandle(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public MainMenuHandle getMainMenu() { + return new MainMenuHandle(guiRobot, primaryStage); + } +} +``` +###### /java/guitests/guihandles/MainMenuHandle.java +``` java +/** + * Provides a handle to the main menu of the app. + */ +public class MainMenuHandle extends GuiHandle { + private static final String HELP = "Help"; + private static final String HELP_MENU_ITEM = "Help Ctrl-H"; + + public MainMenuHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public GuiHandle clickOn(String... menuText) { + Arrays.stream(menuText).forEach((menuItem) -> guiRobot.clickOn(menuItem)); + return this; + } + + public HelpWindowHandle openHelpWindowFromMenu() { + useMenuItemToOpenHelpWindow(); + return new HelpWindowHandle(guiRobot, primaryStage); + } + + public HelpWindowHandle openHelpWindowUsingAccelerator() { + useAcceleratorToOpenHelpWindow(); + return new HelpWindowHandle(guiRobot, primaryStage); + } + + public HelpWindowHandle closeHelpWindowUsingAccelerator() { + useAcceleratorToCloseHelpWindow(); + return new HelpWindowHandle(guiRobot, primaryStage); + } + + public HelpWindowHandle toggleHelpWindow() { + toggleBetweenHelpWindowAndMainWindow(); + return new HelpWindowHandle(guiRobot, primaryStage); + } + + private void useMenuItemToOpenHelpWindow() { + clickOn(HELP, HELP_MENU_ITEM); + } + + private void useAcceleratorToOpenHelpWindow() { + guiRobot.push(new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN)); + guiRobot.sleep(500); + } + + private void useAcceleratorToCloseHelpWindow() { + guiRobot.push(new KeyCodeCombination(KeyCode.ESCAPE)); + guiRobot.sleep(500); + } + + private void toggleBetweenHelpWindowAndMainWindow() { + KeyCodeCombination toggle = new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN); + guiRobot.push(toggle); + guiRobot.sleep(500); + guiRobot.push(toggle); + guiRobot.sleep(500); + } + +} +``` +###### /java/guitests/guihandles/MessageDisplayHandle.java +``` java +/** + * Handler for the message placeholder of the ui + */ +public class MessageDisplayHandle extends GuiHandle{ + + public static final String MESSAGE_PLACEHOLDER_ID = "#messagePlaceHolder"; + + public MessageDisplayHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public String getText() { + return getMessageDisplay() == null ? null : getMessageDisplay().getText(); + } + + private Label getMessageDisplay() { + try { + StackPane messagePlaceHolder = (StackPane)getNode(MESSAGE_PLACEHOLDER_ID); + return (Label) messagePlaceHolder.getChildren().get(0); + } catch (IllegalStateException e) { + return null; + } catch (IndexOutOfBoundsException e) { + return null; + } + } +} +``` +###### /java/guitests/guihandles/ResultDisplayHandle.java +``` java +/** + * A handler for the ResultDisplay of the UI + */ +public class ResultDisplayHandle extends GuiHandle { + + public static final String RESULT_DISPLAY_ID = "#resultDisplay"; + + public ResultDisplayHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public String getText() { + return getResultDisplay().getText(); + } + + private Label getResultDisplay() { + return (Label) getNode(RESULT_DISPLAY_ID); + } +} +``` +###### /java/guitests/guihandles/TaskCardHandle.java +``` java +/** + * Provides a handle to a task card in the task list panel. + */ +public class TaskCardHandle extends GuiHandle { + private static final String NAME_FIELD_ID = "#name"; + private static final String INDEX_FIELD_ID = "#id"; + private static final String TIME_FIELD_ID = "#time"; + private static final String TASK_TIME_PATTERN = "HH:mm EEE, dd MMM"; + private static final String COMPLETED_TIME_PATTERN = "EEE, dd MMM"; + private static final String OVERDUE_PREFIX = "Overdue\n"; + private static final String COMPLETED_PREFIX = "Completed on "; + private static final String START_TIME_PREFIX = "from "; + private static final String END_TIME_PREFIX = " to "; + private static final String DEADLINE_PREFIX = "by "; + private static final String EMPTY_PREFIX = ""; + + private Node node; + + public TaskCardHandle(GuiRobot guiRobot, Stage primaryStage, Node node) { + super(guiRobot, primaryStage, null); + this.node = node; + } + + protected String getTextFromLabel(String fieldId) { + return getTextFromLabel(fieldId, node); + } + + public String getName() { + return getTextFromLabel(NAME_FIELD_ID); + } + + public String getTaskIndex() { + return getTextFromLabel(INDEX_FIELD_ID); + } + + public String getTime() { + return getTextFromLabel(TIME_FIELD_ID); + } + + public boolean isSameTask(ReadOnlyTask task) { + + String name = task.getName().fullName; + + if (!task.isCompleted() && !task.hasTime()) { + return getName().equals(name); + } + + StringBuilder timeDescription = new StringBuilder(); + timeDescription.append(formatTaskTime(task)); + + if (task.isCompleted()) { + timeDescription.append(formatUpdatedTime(task)); + } + + return getName().equals(name) && getTime().equals(timeDescription.toString()); + } + + public String formatTime(String dateTimePattern, String prefix, Optional dateTime) { + + StringBuilder sb = new StringBuilder(); + DateTimeFormatter format = DateTimeFormatter.ofPattern(dateTimePattern); + sb.append(prefix).append(dateTime.get().format(format)); + + return sb.toString(); + } + + public String formatTaskTime(ReadOnlyTask task) { + + StringBuilder timeStringBuilder = new StringBuilder(); + + if (task.isOverdue()) { + timeStringBuilder.append(OVERDUE_PREFIX); + } + + if (task.isEvent()) { + String startTime = formatTime(TASK_TIME_PATTERN, START_TIME_PREFIX, task.getStartDateTime()); + String endTime = formatTime(TASK_TIME_PATTERN, END_TIME_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(startTime); + timeStringBuilder.append(endTime); + } else if (task.hasDeadline()) { + String deadline = formatTime(TASK_TIME_PATTERN, DEADLINE_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(deadline); + } + + return timeStringBuilder.toString(); + } + + public String formatUpdatedTime(ReadOnlyTask task) { + StringBuilder timeStringBuilder = new StringBuilder(); + if (task.hasTime()) { + timeStringBuilder.append("\n"); + } + timeStringBuilder.append(COMPLETED_PREFIX); + timeStringBuilder.append(formatTime(COMPLETED_TIME_PATTERN, EMPTY_PREFIX, + Optional.ofNullable(task.getLastUpdatedTime()))); + return timeStringBuilder.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TaskCardHandle) { + TaskCardHandle handle = (TaskCardHandle) obj; + return getName().equals(handle.getName()) && getTaskIndex().equals(handle.getTaskIndex()) + && getTime().equals(handle.getTime()); + } + return super.equals(obj); + } + + @Override + public String toString() { + return getTaskIndex() + " " + getName() + "Time: " + getTime(); + } + + public String formatTime(ReadOnlyTask task, String dateTimePattern, String prefix, + Optional dateTime) { + + StringBuilder sb = new StringBuilder(); + DateTimeFormatter format = DateTimeFormatter.ofPattern(dateTimePattern); + + if (task.isCompleted()) { + sb.append(dateTime.get().format(format)); + } else if (dateTime.isPresent() && task.getStartDateTime().isPresent()) { + sb.append(prefix).append(dateTime.get().format(format)); + } else if (dateTime.isPresent()) { + sb.append(DEADLINE_PREFIX).append(dateTime.get().format(format)); + } else { + sb.append(EMPTY_PREFIX); + } + + return sb.toString().toLowerCase(); + } +} +``` +###### /java/guitests/HelpWindowTest.java +``` java +public class HelpWindowTest extends ToDoListGuiTest { + + @Test + public void openHelpWindow() { + + assertHelpWindowOpen(mainMenu.openHelpWindowFromMenu()); + + assertHelpWindowOpen(mainMenu.openHelpWindowUsingAccelerator()); + + assertHelpWindowOpen(commandBox.runHelpCommand()); + + } + + @Test + public void closeHelpWindow() { + commandBox.runHelpCommand(); + assertHelpWindowClose(mainMenu.closeHelpWindowUsingAccelerator()); + } + + // Tests Ctrl-H to switch between mainwindow and helpwindow + @Test + public void toggleHelpWindow() { + assertHelpWindowClose(mainMenu.toggleHelpWindow()); + } + + private void assertHelpWindowClose(HelpWindowHandle helpWindowHandle) { + assertTrue(helpWindowHandle.isWindowClose()); + } + + private void assertHelpWindowOpen(HelpWindowHandle helpWindowHandle) { + assertTrue(helpWindowHandle.isWindowOpen()); + helpWindowHandle.closeWindow(); + } +} +``` +###### /java/guitests/MarkCommandTest.java +``` java +public class MarkCommandTest extends ToDoListGuiTest{ + + @Test + public void mark_nonEmptytask_succeed() { + TestTask[] currentList = td.getTypicalTasks(); + currentList[0].markAsCompleted(); + TestTask taskToMark = currentList[0]; + assertMarkSuccess("mark 1", taskToMark, currentList); + } + + @Test + public void mark_nonEmptytask_duplicates() { + assertMarkDuplicates("mark 7"); + } + + @Test + public void mark_emptytask() { + assetMarkEmptyTask("mark 8"); + } + + private void assertMarkSuccess(String command, TestTask taskToMark, TestTask... currentList) { + commandBox.runCommand(command); + + //confirm the new card contains the right data + if (taskToMark.isCompleted()) { + TaskCardHandle addedCard = completedTasksPanel.navigateToTask(taskToMark.getName().fullName); + assertMatching(taskToMark, addedCard); + } else if (!taskToMark.isCompleted() && !taskToMark.hasTime()) { + TaskCardHandle addedCard = floatingTasksPanel.navigateToTask(taskToMark.getName().fullName); + assertMatching(taskToMark, addedCard); + } else if (!taskToMark.isCompleted() && taskToMark.hasTime()) { + TaskCardHandle addedCard = upcomingTasksPanel.navigateToTask(taskToMark.getName().fullName); + assertMatching(taskToMark, addedCard); + } + + //confirm the list now contains all previous tasks plus the new task + taskToMark.setLastUpdatedTimeToNow(); + TestTask[] expectedList = currentList; + assertAllPanelsMatch(expectedList); + assertResultMessage(MarkCommand.MESSAGE_MARK_TASK_SUCCESS); + } + + private void assertMarkDuplicates(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_DUPLICATE_TASK); + } + + private void assetMarkEmptyTask(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } +} +``` +###### /java/guitests/ToDoListGuiTest.java +``` java + /** + * Asserts the message shown in the Result Display area is same as the given string. + */ + protected void assertResultMessage(String expected) { + assertEquals(expected, resultDisplay.getText()); + } + + /** + * Asserts the message shown in the Message Display area is same as the given string. + */ + protected void assertShowingMessage(String expected) { + assertEquals(expected, messageDisplay.getText()); + } + +``` +###### /java/guitests/UnmarkCommandTest.java +``` java +public class UnmarkCommandTest extends ToDoListGuiTest { + @Test + public void unmark_nonEmptytask_succeed() { + TestTask[] currentList = td.getTypicalTasks(); + TestTask taskToUnmark = currentList[0]; + commandBox.runCommand("mark 1"); + assertUnmarkSuccess("unmark 7", taskToUnmark, currentList); + } + + @Test + public void unmark_nonEmptytask_duplicates() { + assertUnmarkDuplicates("unmark 1"); + } + + @Test + public void unmark_emptytask() { + assetUnmarkEmptyTask("unmark 8"); + } + + private void assertUnmarkSuccess(String command, TestTask taskToUnmark, TestTask... currentList) { + commandBox.runCommand(command); + + //confirm the new card contains the right data + if (taskToUnmark.isCompleted()) { + TaskCardHandle addedCard = completedTasksPanel.navigateToTask(taskToUnmark.getName().fullName); + assertMatching(taskToUnmark, addedCard); + } else if (!taskToUnmark.isCompleted() && !taskToUnmark.hasTime()) { + TaskCardHandle addedCard = floatingTasksPanel.navigateToTask(taskToUnmark.getName().fullName); + assertMatching(taskToUnmark, addedCard); + } else if (!taskToUnmark.isCompleted() && taskToUnmark.hasTime()) { + TaskCardHandle addedCard = upcomingTasksPanel.navigateToTask(taskToUnmark.getName().fullName); + assertMatching(taskToUnmark, addedCard); + } + + //confirm the list now contains all previous tasks plus the new task + taskToUnmark.setLastUpdatedTimeToNow(); + TestTask[] expectedList = currentList; + assertAllPanelsMatch(expectedList); + assertResultMessage(UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS); + } + + private void assertUnmarkDuplicates(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_DUPLICATE_TASK); + } + + private void assetUnmarkEmptyTask(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } +} +``` diff --git a/collated/test/A0148095X.md b/collated/test/A0148095X.md new file mode 100644 index 000000000000..f2c924922a90 --- /dev/null +++ b/collated/test/A0148095X.md @@ -0,0 +1,1267 @@ +# A0148095X +###### /java/guitests/LoadCommandTest.java +``` java +public class LoadCommandTest extends ToDoListGuiTest { + + private final String command = LoadCommand.COMMAND_WORD + " "; + private final String fileThatExists = "data/test/FileThatExists.xml"; + private final String fileThatDoesNotExist = "data/test/DoesNotExist.xml"; + private final String fileInWrongFormat = "data/test/WrongFormat.xml"; + private final String missingFileType = "data/test/invalid"; + private final String missingFileName = "data/test/.bad"; + + @Before + public void setUp() throws Exception { + super.setUp(); + + // setup storage file + Task toBeAdded = new Task(new Name("test")); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + XmlToDoListStorage xmltdls = new XmlToDoListStorage(fileThatExists); + xmltdls.saveToDoList(expectedTDL); + + // create empty file + FileUtil.createFile(new File(fileInWrongFormat)); + } + + @After + public void clean() throws Exception { + // cleanup + FileUtil.deleteFile(fileThatExists); + FileUtil.deleteFile(fileInWrongFormat); + } + + @Test + public void load_pathValidFileExists_messageSuccess() { + // load from an existing file + commandBox.runCommand(command + fileThatExists); + assertResultMessage(String.format(LoadCommand.MESSAGE_SUCCESS, fileThatExists)); + } + + @Test + public void load_pathValidFileDoesNotExist_messageFileDoesNotExist() { + // load from a non-existing file + commandBox.runCommand(command + fileThatDoesNotExist); + assertResultMessage(String.format(LoadCommand.MESSAGE_FILE_DOES_NOT_EXIST, fileThatDoesNotExist)); + } + + @Test + public void load_pathValidFileWrongFormat_messageFileWrongFormat() { + // file in wrong format + commandBox.runCommand(command + fileInWrongFormat); + assertResultMessage(String.format(LoadCommand.MESSAGE_FILE_WRONG_FORMAT, fileInWrongFormat)); + } + + @Test + public void load_fileTypeInvalid_messagePathInvalid() { + // invalid file type + commandBox.runCommand(command + missingFileType); + assertResultMessage(String.format(LoadCommand.MESSAGE_PATH_INVALID, missingFileType)); + } + + @Test + public void load_fileNameInvalid_messagePathInvalid() { + // invalid file name + commandBox.runCommand(command + missingFileName); + assertResultMessage(String.format(LoadCommand.MESSAGE_PATH_INVALID, missingFileName)); + } +} +``` +###### /java/guitests/StoreCommandTest.java +``` java +public class StoreCommandTest extends ToDoListGuiTest { + + private final String validLocation = "data/test.xml"; + private final String badLocation = "test/.xml"; + private final String inaccessibleLocation = "C:/windows/system32/agendum/todolist.xml"; + + @Test + public void store_validLocation_messageSuccess() { + //save to a valid directory + commandBox.runCommand("store " + validLocation); + assertResultMessage(String.format(StoreCommand.MESSAGE_SUCCESS, validLocation)); + } + + @Test + public void store_defaultLocation_messageSuccessDefaultLocation() { + //save to default directory + commandBox.runCommand("store default"); + assertResultMessage(String.format(StoreCommand.MESSAGE_LOCATION_DEFAULT, Config.DEFAULT_SAVE_LOCATION)); + } + + @Test + public void store_invalidLocation_messageInvalidPath() { + //invalid Location + commandBox.runCommand("store " + badLocation); + assertResultMessage(StoreCommand.MESSAGE_PATH_WRONG_FORMAT); + } + + @Test + public void store_inaccessibleLocation_messageLocationInaccessible() { + //inaccessible location + commandBox.runCommand("store " + inaccessibleLocation); + //assertResultMessage(StoreCommand.MESSAGE_LOCATION_INACCESSIBLE); + } + + @Test + public void store_fileExists_messageFileExists() throws IOException, FileDeletionException { + //file exists + FileUtil.createIfMissing(new File(validLocation)); + commandBox.runCommand("store " + validLocation); + assertResultMessage(StoreCommand.MESSAGE_FILE_EXISTS); + FileUtil.deleteFile(validLocation); + + } +} +``` +###### /java/seedu/agendum/commons/core/ConfigTest.java +``` java + @Test + public void setAliasTableFilePath_validPath_returnsTrue() { + Config config = new Config(); + String validPath = "dropbox/table.xml"; + config.setAliasTableFilePath(validPath); + + assertEquals(validPath, config.getAliasTableFilePath()); + } + + @Test + public void equals_differentObjectType_returnsFalse() { + assertFalse(one.equals(new Object())); + } + + @Test + public void equals_same_returnsTrue() { + assertTrue(one.equals(one)); + } + + @Test + public void equals_symmetric_returnsTrue() { + assertTrue(one.equals(another) && another.equals(one)); + } + + @Test + public void hashCode_symmetric_returnsTrue() { + assertTrue(one.hashCode() == another.hashCode()); + } +} +``` +###### /java/seedu/agendum/commons/core/GuiSettingsTest.java +``` java +public class GuiSettingsTest { + + private Double windowWidth = 800.0; + private Double windowHeight = 600.0; + private int xPosition = 100; + private int yPosition = 300; + + private GuiSettings one, another; + + @Before + public void setUp() { + one = new GuiSettings(windowWidth, windowHeight, xPosition, yPosition); + another = new GuiSettings(windowWidth, windowHeight, xPosition, yPosition); + } + + @Test + public void equals_differentObject_returnsFalse() { + assertFalse(one.equals(new Object())); + } + + @Test + public void equals_symmetric_returnsTrue() { + // equals to itself and similar object + assertTrue(one.equals(one)); + assertTrue(one.equals(another)); + } + + @Test + public void equals_validInputDifferentSettings_returnsFalse() { + // ----------- different settings ---------------- + GuiSettings differentSettings; + + Double differentWindowWidth = windowWidth*2; + Double differentWindowHeight = windowHeight*2; + int differentXPosition = xPosition*2; + int differentYPosition = yPosition*2; + + // different width + differentSettings = new GuiSettings(differentWindowWidth, windowHeight, xPosition, yPosition); + assertFalse(one.equals(differentSettings)); + + // different height + differentSettings = new GuiSettings(windowWidth, differentWindowHeight, xPosition, yPosition); + assertFalse(one.equals(differentSettings)); + + // different x position + differentSettings = new GuiSettings(windowWidth, windowHeight, differentXPosition, yPosition); + assertFalse(one.equals(differentSettings)); + + // different y position + differentSettings = new GuiSettings(windowWidth, windowHeight, xPosition, differentYPosition); + assertFalse(one.equals(differentSettings)); + } + + @Test + public void hashcode_symmetric_returnsTrue() { + assertEquals(one.hashCode(), another.hashCode()); + } +} +``` +###### /java/seedu/agendum/commons/core/VersionTest.java +``` java + @Test + public void versionComparableNotEqual() { + Version original = new Version(0, 0, 0, false); + + // null + Object nullObj = null; + assertFalse(original.equals(nullObj)); + + // Different object + Object obj = new Object(); + assertFalse(original.equals(obj)); + } +``` +###### /java/seedu/agendum/commons/util/CollectionUtilTest.java +``` java +public class CollectionUtilTest { + + public String string = "string"; + public int number = 1; + public double decimal = 2.0; + public boolean bool = true; + public Object obj = new Object(); + + public ArrayList noNullUniqueArrayList; + + @Before + public void setUp() { + noNullUniqueArrayList = new ArrayList(); + noNullUniqueArrayList.add(string); + noNullUniqueArrayList.add(number); + noNullUniqueArrayList.add(decimal); + noNullUniqueArrayList.add(bool); + noNullUniqueArrayList.add(obj); + } + + @Test + public void isNotNull() { + // No nulls + assertTrue(CollectionUtil.isNotNull(string, number, decimal, bool, obj)); + + // One null + assertFalse(CollectionUtil.isNotNull(string, number, null, decimal, bool, obj)); + } + + @Test + public void assertNoNullNoNull() { + // No assertion errors; all non-null + CollectionUtil.assertNoNullElements(noNullUniqueArrayList); + } + + @Test(expected = AssertionError.class) + public void assertNoNullElementsNullCollection(){ + // The collection is null + CollectionUtil.assertNoNullElements(null); + } + + @Test + public void elementsAreUnique() { + // Unique + assertTrue(CollectionUtil.elementsAreUnique(noNullUniqueArrayList)); + + // Not unique + ArrayList notUniqueArrayList = new ArrayList<>(noNullUniqueArrayList); + notUniqueArrayList.add(string); + assertFalse(CollectionUtil.elementsAreUnique(notUniqueArrayList)); + } +} +``` +###### /java/seedu/agendum/commons/util/FileUtilTest.java +``` java + @Before + public void setUp() throws IOException { + filePathThatExists = "file_that_exists.test"; + fileThatExists = new File(filePathThatExists); + FileUtil.createFile(fileThatExists); + + filePathThatDoesNotExist = "file_that_does_not_exist.test"; + fileThatDoesNotExist = new File(filePathThatDoesNotExist); + + filePathToBeDeleted = "file_to_be_deleted.test"; + fileToBeDeleted = new File(filePathToBeDeleted); + FileUtil.createFile(fileToBeDeleted); + + filePathWithParentDirectories = "data/test/file_with_parent_directories.test"; + fileWithParentDirectories = new File(filePathWithParentDirectories); + + filePathWithInvalidDirectoryName = "invalid \0 /0 directory"; + fileWithInvalidDirectoryName = new File(filePathWithInvalidDirectoryName); + + filePathWithValidDirectoryName = "validdirectory"; + fileWithValidDirectoryName = new File(filePathWithValidDirectoryName); + } + + @After + public void cleanup() throws IOException { + fileThatExists.delete(); + fileThatDoesNotExist.delete(); + fileToBeDeleted.delete(); + fileWithParentDirectories.delete(); + fileWithInvalidDirectoryName.delete(); + fileWithValidDirectoryName.delete(); + } + + @Test + public void isFileExists_validPathFileDoesNotExist_returnsFalse() { + assertFalse(FileUtil.isFileExists(filePathThatDoesNotExist)); + assertFalse(FileUtil.isFileExists(fileThatDoesNotExist)); + } + + @Test + public void isFileExists_fileExists_returnsTrue() { + assertTrue(FileUtil.isFileExists(filePathThatExists)); + assertTrue(FileUtil.isFileExists(fileThatExists)); + } + + @Test + public void createFile_validFilePathWithParentDirectories() throws IOException, FileDeletionException { + assertTrue(FileUtil.createFile(fileWithParentDirectories)); + // create file again to test when it already exists + assertFalse(FileUtil.createFile(fileWithParentDirectories)); + FileUtil.deleteFile(filePathWithParentDirectories); + } + + @Test(expected = AssertionError.class) + public void deleteFile_nullFilePath_throwsAssertionError() throws FileDeletionException { + // invalid filepath + FileUtil.deleteFile(null); + } + + @Test + public void deleteFile_validPathAndfile_success() throws FileDeletionException { + FileUtil.deleteFile(filePathToBeDeleted); + assertFalse(FileUtil.isFileExists(filePathToBeDeleted)); + } + + @Test (expected = FileDeletionException.class) + public void deleteFile_validPathInvalidFile_throwsFileDeletionException() throws FileDeletionException { + FileUtil.deleteFile(filePathThatDoesNotExist); + } + + @Test + public void isPathAvailable_validPathExistingFile_returnsTrue() { + assertTrue(FileUtil.isPathAvailable(filePathThatExists)); + } + + @Test + public void isPathAvailable_validPathNonExistingFile_returnsTrue() { + assertTrue(FileUtil.isPathAvailable(filePathThatDoesNotExist)); + } + + @Test + public void isPathAvailable_invalidPath_returnsFalse() { + assertFalse(FileUtil.isPathAvailable(filePathWithInvalidDirectoryName)); + } + + + @Test (expected = IOException.class) + public void createDirs_invalidDirectoryName_throwsIOException() throws IOException { + FileUtil.createDirs(fileWithInvalidDirectoryName); + } + + @Test + public void createDirs_validDirectoryName_success() throws IOException { + FileUtil.createDirs(fileWithValidDirectoryName); + } +``` +###### /java/seedu/agendum/commons/util/StringUtilTest.java +``` java + /* + * Valid equivalence partitions for path to file: + * - file path is valid + * - file name is valid + * - file type is valid + * + * Possible scenarios returning true: + * - valid relative path to a file + * - valid absolute path to a file for windows + * - valid absolute path to a file for Unix/MacOS + * + * Possible scenarios returning false: + * - file name missing + * - file type missing + * - null path + * - empty path + * - file path not in the right format + * + * The test method below tries to verify all above with a reasonably low number of test cases. + */ + @Test + public void isValidPathToFile(){ + // null and empty file paths + assertFalse(StringUtil.isValidPathToFile(null)); // null path + assertFalse(StringUtil.isValidPathToFile("")); // empty path + + // relative file paths + assertFalse(StringUtil.isValidPathToFile("a")); // missing file type + assertFalse(StringUtil.isValidPathToFile("data/.xml")); // invalid file name + assertFalse(StringUtil.isValidPathToFile("data /valid.xml")); // invalid file path with spaces after + + assertTrue(StringUtil.isValidPathToFile("Program Files/data.xml")); // valid path to file with acceptable spaces in file path + + // absolute file paths for windows + assertFalse(StringUtil.isValidPathToFile("1:/data.xml")); // invalid drive + assertFalse(StringUtil.isValidPathToFile("C:/data/a")); // invalid file type + assertFalse(StringUtil.isValidPathToFile("C:/data/.xml")); // invalid file name + assertFalse(StringUtil.isValidPathToFile("C:/ data/valid.xml")); // invalid file path with spaces before + + assertTrue(StringUtil.isValidPathToFile("Z:/Program Files/some-other-folder/data.dat")); // valid drive, folder and file name + + // absolute file path for unix/MacOX + assertFalse(StringUtil.isValidPathToFile("/usr/data")); // invalid file type + assertFalse(StringUtil.isValidPathToFile("/usr/.xml")); // invalid file name + assertFalse(StringUtil.isValidPathToFile("/ usr/data.xml")); // invalid file path with spaces before + + assertTrue(StringUtil.isValidPathToFile("/usr/bin/my folder/data.xml")); // valid folder and file name with spaces + } + +} +``` +###### /java/seedu/agendum/logic/DateTimeUtilsTest.java +``` java + @Test + public void parseNaturalLanguageDateTimeString_emptyInput_emptyOptional() { + Optional parsed = DateTimeUtils.parseNaturalLanguageDateTimeString(""); + assertFalse(parsed.isPresent()); + } + + @Test + public void parseNaturalLanguageDateTimeString_nullInput_emptyOptional() { + Optional parsed = DateTimeUtils.parseNaturalLanguageDateTimeString(null); + assertFalse(parsed.isPresent()); + } + + @Test + public void parseNaturalLanguageDateTimeString_inputNoGroups_emptyOptional() { + Optional parsed = DateTimeUtils.parseNaturalLanguageDateTimeString("asd"); + assertFalse(parsed.isPresent()); + } + +} +``` +###### /java/seedu/agendum/logic/LogicManagerTest.java +``` java + @Test + public void execute_store_successful() throws Exception { + // setup expectations + ToDoList expectedTDL = new ToDoList(); + Task testTask = new Task(new Name("test_store")); + expectedTDL.addTask(testTask); + model.addTask(testTask); + + String location = "data/test_store_successful.xml"; + CommandResult result; + String inputCommand; + String feedback; + EventsCollector eventCollector = new EventsCollector(); + + // execute command and verify result + inputCommand = "store " + location; + result = logic.execute(inputCommand); + feedback = String.format(StoreCommand.MESSAGE_SUCCESS, location); + assertEquals(feedback, result.feedbackToUser); + assertTrue(eventCollector.get(0) instanceof ChangeSaveLocationEvent); + assertTrue(eventCollector.get(1) instanceof ToDoListChangedEvent); + + // execute command and verify result + inputCommand = "store default"; + result = logic.execute(inputCommand); + feedback = String.format(StoreCommand.MESSAGE_LOCATION_DEFAULT, Config.DEFAULT_SAVE_LOCATION); + assertEquals(feedback, result.feedbackToUser); + assertTrue(eventCollector.get(2) instanceof ChangeSaveLocationEvent); + assertTrue(eventCollector.get(3) instanceof ToDoListChangedEvent); + } + + @Test + public void execute_store_fileExists_fail() throws Exception { + // setup expectations + ToDoList expectedTDL = new ToDoList(); + String location = "data/test_store_fail.xml"; + + // create file + FileUtil.createIfMissing(new File(location)); + + // error that file already exists + assertCommandBehavior("store " + location, + String.format(StoreCommand.MESSAGE_FILE_EXISTS, location), + expectedTDL, + expectedTDL.getTaskList()); + + // delete file + FileUtil.deleteFile(location); + } + + @Test + public void execute_exit_success() { + CommandResult result = logic.execute(ExitCommand.COMMAND_WORD); + assertEquals(ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT, result.feedbackToUser); + } +``` +###### /java/seedu/agendum/logic/LogicManagerTest.java +``` java + @Test + public void executeLoad_fileExists_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.generateTask(999); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + model.addTask(toBeAdded); + + // setup storage file + String filePath = "data/test/load.xml"; + XmlToDoListStorage xmltdls = new XmlToDoListStorage(filePath); + xmltdls.saveToDoList(expectedTDL); + + // execute command and verify result + assertCommandBehavior("load " + filePath, + String.format(LoadCommand.MESSAGE_SUCCESS, filePath), + expectedTDL, + expectedTDL.getTaskList()); + + FileUtil.deleteFile(filePath); + } + + @Test + public void executeLoad_fileDoesNotExist_fail() throws Exception { + // setup expectations + ToDoList expectedTDL = new ToDoList(); + String filePath = "data/test/loadDoesNotExist.xml"; + + // execute command and verify result + assertCommandBehavior("load " + filePath, + String.format(LoadCommand.MESSAGE_FILE_DOES_NOT_EXIST, filePath), + expectedTDL, + expectedTDL.getTaskList()); + } +``` +###### /java/seedu/agendum/MainAppTest.java +``` java +public class MainAppTest { + + private MainApp mainApp; + + private Config defaultConfig; + private UserPrefs defaultUserPrefs; + private Hashtable defaultAliasTable; + + // user prefs and alias table filepaths lead to empty files + private Config configWithBadFilePaths; + // user prefs and alias table filepaths lead to read only files + private Config configWithReadOnlyFilePaths; + + private final String pathToBadConfig = TestUtil.getFilePathInSandboxFolder("bad_config.json"); + private final String pathToReadOnlyConfig = TestUtil.getFilePathInSandboxFolder("read_only_config.json"); + + private final String pathToBadUserPrefs = TestUtil.getFilePathInSandboxFolder("bad_user_prefs.json"); + private final String pathToReadOnlyUserPrefs = TestUtil.getFilePathInSandboxFolder("read_only_user_prefs.json"); + + private final String pathToBadAliasTable = TestUtil.getFilePathInSandboxFolder("bad_alias_table.json"); + private final String pathToReadOnlyAliasTable = TestUtil.getFilePathInSandboxFolder("read_only_alias_table.json"); + + @Before + public void setUp() { + mainApp = new MainApp(); + + defaultConfig = new Config(); + defaultUserPrefs = new UserPrefs(); + defaultAliasTable = new Hashtable(); + + configWithBadFilePaths = generateConfigWithBadFilePaths(); + configWithReadOnlyFilePaths = generateConfigWithReadOnlyFilePaths(); + + createEmptyFile(pathToBadConfig); + createReadOnlyConfigFile(pathToReadOnlyConfig); + + createEmptyFile(pathToBadUserPrefs); + createReadOnlyUserPrefsFile(pathToReadOnlyUserPrefs); + + createEmptyFile(pathToBadAliasTable); + createReadOnlyAliasTableFile(pathToReadOnlyAliasTable); + } + + @Test + public void initConfig_nullFilePath_returnsDefaultConfig() { + Config config = mainApp.initConfig(null); + assertEquals(config, defaultConfig); + } + + @Test + public void initConfig_validFilePathInvalidFileFormat_returnsDefaultConfig() { + Config config = mainApp.initConfig(pathToBadConfig); + assertEquals(config, defaultConfig); + } + + @Test + public void initConfig_validFilePathValidFormatReadOnly_returnsDefaultConfigLogsWarning() { + Config config = mainApp.initConfig(pathToReadOnlyConfig); + assertEquals(config, defaultConfig); + } + + @Test + public void initPrefs_invalidFileFormat_returnsDefaultUserPrefs() { + // Set up storage to point to bad user prefs + mainApp.storage = new StorageManager("", "", pathToBadUserPrefs, null); + + UserPrefs userPrefs = mainApp.initPrefs(configWithBadFilePaths); + assertEquals(userPrefs, defaultUserPrefs); + + // reset storage + mainApp.storage = null; + } + + @Test + public void initPrefs_validFileReadOnly_returnsDefaultUserPrefsLogsWarning() { + // Set up storage to point to read only prefs + mainApp.storage = new StorageManager("", "", pathToReadOnlyUserPrefs, null); + + UserPrefs userPrefs = mainApp.initPrefs(configWithReadOnlyFilePaths); + assertEquals(userPrefs, defaultUserPrefs); + + // reset storage + mainApp.storage = null; + } + + @Test + public void initPrefs_exceptionThrowingStorage_returnsDefaultUserPrefsLogsWarning() { + mainApp.storage = new ReadUserPrefsExceptionThrowingStorageManagerStub(); + + UserPrefs userPrefs = mainApp.initPrefs(defaultConfig); + assertEquals(userPrefs, defaultUserPrefs); + + // reset storage + mainApp.storage = null; + } + + @Test + public void initAliasTable_invalidFileFormat_returnsEmptyHashtable() { + // Set up storage to point to bad alias table + mainApp.storage = new StorageManager("", pathToBadAliasTable, "", null); + + mainApp.initAliasTable(configWithBadFilePaths); + Hashtable actualAliasTable = CommandLibrary.getInstance().getAliasTable(); + assertEquals(actualAliasTable, defaultAliasTable); + + // reset storage and alias table + mainApp.storage = null; + CommandLibrary.getInstance().loadAliasTable(null); + } + + @Test + public void initAliasTable_validFileFormatReadOnly_returnsEmptyHashtable() { + // Set up storage to point to read only alias table + mainApp.storage = new StorageManager("", pathToReadOnlyAliasTable, "", null); + + mainApp.initAliasTable(configWithReadOnlyFilePaths); + Hashtable actualAliasTable = CommandLibrary.getInstance().getAliasTable(); + assertEquals(actualAliasTable, defaultAliasTable); + + // reset storage and alias table + mainApp.storage = null; + CommandLibrary.getInstance().loadAliasTable(null); + } + + @Test + public void initAliasTable_exceptionThrowingStorage_returnsDefaultHashtableLogsWarning() { + mainApp.storage = new ReadAliasTableExceptionThrowingStorageManagerStub(); + + mainApp.initAliasTable(defaultConfig); + Hashtable actualAliasTable = CommandLibrary.getInstance().getAliasTable(); + assertEquals(actualAliasTable, defaultAliasTable); + + // reset storage and alias table + mainApp.storage = null; + CommandLibrary.getInstance().loadAliasTable(null); + } + + private void createEmptyFile(String filePath) { + File file = new File(filePath); + + deleteIfExists(file); + + try { + file.createNewFile(); + } catch (IOException e) { + Assert.fail("Error creating empty file at: " + filePath); + } + } + + private void createReadOnlyConfigFile(String filePath) { + File file = new File(filePath); + + // Ensure that the file is empty + deleteIfExists(file); + + try { + ConfigUtil.saveConfig(defaultConfig, filePath); + } catch (IOException e) { + Assert.fail("Error creating read only config file"); + } + + if (!file.setReadOnly()) { + Assert.fail("Unable to set read only config to read only"); + } + } + + private void createReadOnlyUserPrefsFile(String filePath) { + File file = new File(filePath); + + // Ensure that the file is empty + deleteIfExists(file); + + try { + JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(filePath); + userPrefsStorage.saveUserPrefs(defaultUserPrefs, filePath); + } catch (IOException e) { + Assert.fail("Error creating read only user prefs file"); + } + + if (!file.setReadOnly()) { + Assert.fail("Unable to set read only user prefs to read only"); + } + } + + private void createReadOnlyAliasTableFile(String filePath) { + File file = new File(filePath); + + // Ensure that the file is empty + deleteIfExists(file); + + try { + JsonAliasTableStorage aliasTableStorage = new JsonAliasTableStorage(filePath); + aliasTableStorage.saveAliasTable(defaultAliasTable, filePath); + } catch (IOException e) { + Assert.fail("Error creating read only alias table file"); + } + + if (!file.setReadOnly()) { + Assert.fail("Unable to set read only alias table to read only"); + } + } + + public void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + private Config generateConfigWithReadOnlyFilePaths() { + Config config = new Config(); + config.setAliasTableFilePath(pathToReadOnlyAliasTable); + config.setUserPrefsFilePath(pathToReadOnlyUserPrefs); + return config; + } + + private Config generateConfigWithBadFilePaths() { + Config config = new Config(); + config.setAliasTableFilePath(pathToBadAliasTable); + config.setUserPrefsFilePath(pathToBadUserPrefs); + return config; + } + + /** Throws an IOException when readUserPrefs is called **/ + class ReadUserPrefsExceptionThrowingStorageManagerStub extends StorageManager { + public ReadUserPrefsExceptionThrowingStorageManagerStub() { + super("", "", "", null); + } + + public Optional readUserPrefs() throws IOException { + throw new IOException(this.getClass().getCanonicalName() +": IOException"); + } + } + + /** Throws an IOException when readAliasTable is called **/ + class ReadAliasTableExceptionThrowingStorageManagerStub extends StorageManager { + public ReadAliasTableExceptionThrowingStorageManagerStub() { + super("", "", "", null); + } + + @Override + public Optional> readAliasTable() throws DataConversionException, IOException { + throw new IOException(this.getClass().getCanonicalName() + ": IOException"); + } + } +} +``` +###### /java/seedu/agendum/model/NameTest.java +``` java +public class NameTest { + private String invalidNameString = "Vishnu \n Rachael \n Weigang"; + private String validNameString = "Justin"; + + private Name one; + private Name another; + + @Before + public void setUp() throws IllegalValueException { + one = new Name(validNameString); + another = new Name(validNameString); + } + + @Test + public void equals_symmetric_returnsTrue() throws IllegalValueException { + assertTrue(one.equals(another) && another.equals(one)); + } + + @Test + public void hashCode_symmetric_returnsTrue() throws IllegalValueException { + assertTrue(one.hashCode() == another.hashCode()); + } + + @SuppressWarnings("unused") + @Test (expected = IllegalValueException.class) + public void name_invalid_throwsIllegalValueException() throws IllegalValueException { + Name name = new Name(invalidNameString); + } +} +``` +###### /java/seedu/agendum/model/ToDoListTest.java +``` java +public class ToDoListTest { + + private Task alice, bob, charlie; + private ToDoList one, another; + + @Before + public void setUp() throws IllegalValueException{ + alice = new Task(new Name("meet alice")); + bob = new Task(new Name("meet bob")); + charlie = new Task(new Name("meet charlie")); + + one = new ToDoList(); + one.addTask(alice); + one.addTask(bob); + + another = new ToDoList(); + another.addTask(alice); + another.addTask(bob); + } + + + @Test + public void equals_symmetric_returnsTrue() { + assertTrue(one.equals(another) && another.equals(one)); + } + + @Test + public void hashCode_symmetric_returnsTrue() { + assertTrue(one.hashCode() == another.hashCode()); + } + + @Test + public void getEmptyToDoList() { + assertTrue(ToDoList.getEmptyToDoList().getTaskList().isEmpty()); + } + + @Test + public void removeTask_taskExists_removedSuccessfully() throws TaskNotFoundException { + one.removeTask(alice); + assertFalse(one.getTaskList().contains(alice)); + } + + @Test (expected = TaskNotFoundException.class) + public void removeTask_taskDoesNotExist_throwsTaskNotFoundException() throws TaskNotFoundException { + one.removeTask(charlie); + } +} +``` +###### /java/seedu/agendum/model/UnmodifiableObservableListTest.java +``` java + @Test + public void mutatingMethodsDisabled() { + + final Class ex = UnsupportedOperationException.class; + + assertThrows(ex, () -> list.add(3)); + assertThrows(ex, () -> list.add(1, 2)); + assertThrows(ex, () -> list.add(1, null)); + + assertThrows(ex, () -> list.addAll(2, 1)); + assertThrows(ex, () -> list.addAll(null, 1)); + assertThrows(ex, () -> list.addAll(backing)); + assertThrows(ex, () -> list.addAll(0, backing)); + + assertThrows(ex, () -> list.set(0, 2)); + assertThrows(ex, () -> list.set(1, null)); + + assertThrows(ex, () -> list.setAll(1, 2)); + assertThrows(ex, () -> list.setAll(1, null)); + assertThrows(ex, () -> list.setAll(backing)); + + assertThrows(ex, () -> list.remove(0, 1)); + assertThrows(ex, () -> list.remove(null)); + assertThrows(ex, () -> list.remove(0)); + + assertThrows(ex, () -> list.removeAll(backing)); + assertThrows(ex, () -> list.removeAll(1, 2)); + assertThrows(ex, () -> list.removeAll(null, 2)); + + assertThrows(ex, () -> list.retainAll(backing)); + assertThrows(ex, () -> list.retainAll(1, 2)); + assertThrows(ex, () -> list.retainAll(1, null)); + + assertThrows(ex, () -> list.replaceAll(i -> 1)); + + assertThrows(ex, () -> list.sort(Comparator.naturalOrder())); + assertThrows(ex, () -> list.sort(null)); + + assertThrows(ex, () -> list.clear()); + + final Iterator iter = list.iterator(); + iter.next(); + assertThrows(ex, iter::remove); + + final ListIterator liter = list.listIterator(); + liter.next(); + assertThrows(ex, liter::remove); + assertThrows(ex, () -> liter.add(5)); + assertThrows(ex, () -> liter.set(3)); + assertThrows(ex, () -> list.removeIf(i -> true)); + } + + @SuppressWarnings("unused") + @Test (expected = NullPointerException.class) + public void unmodifiableObservableList_nullList_nullPointerExceptionThrown() { + UnmodifiableObservableList nullList = new UnmodifiableObservableList<>(null); + } + + @Test + public void isEmpty_nonEmptyList_returnsFalse() { + assertFalse(list.isEmpty()); + } + + @Test + public void contains_existingItem_returnsTrue() { + assertTrue(list.contains(ITEM_TWO)); + } + + @Test + public void containsAll_sameList_returnsTrue() { + assertTrue(list.containsAll(backing)); + } + + @Test + public void containsAll_listWhichContainsOneExtraItem_returnsFalse() { + final List backingWithOneExtraItem = new ArrayList<>(backing); + backingWithOneExtraItem.add(15); + final UnmodifiableObservableList listWithOneExtraItem = new UnmodifiableObservableList<>(FXCollections.observableList(backingWithOneExtraItem)); + + assertFalse(list.containsAll(listWithOneExtraItem)); + } + + @Test + public void indexOf_validItem_returnsCorrectIndex() { + final int itemToFind = ITEM_ONE; + final int index = list.indexOf(itemToFind); + + assertEquals(index, 1); + } + + @Test + public void indexOf_invalidItem_returnsErrorIndex() { + final int itemToFind = 999; + final int index = list.indexOf(itemToFind); + + assertEquals(index, -1); + } + + @Test + public void lastIndexOf_validItem_returnsCorrectIndex() { + final int itemToFind = ITEM_ZERO; + final int itemToAdd = ITEM_ZERO; // add a duplicate so it become last object + + final List backingWithDuplicate = new ArrayList<>(backing); + backingWithDuplicate.add(itemToAdd); + final UnmodifiableObservableList listWithDuplicate = new UnmodifiableObservableList<>(FXCollections.observableList(backingWithDuplicate)); + + final int expectedIndex = listWithDuplicate.size()-1; + final int actualIndex = listWithDuplicate.lastIndexOf(itemToFind); + + assertEquals(expectedIndex, actualIndex); + } + + @Test + public void lastIndexOf_invalidItem_returnsErrorIndex() { + final int itemToFind = 888; + final int index = list.lastIndexOf(itemToFind); + + assertEquals(index, -1); + } + + @Test + public void subList_sameItems_returnsTrue() { + final int startIndex = 1; + final int endIndex = 3; + List subListOfBacking = backing.subList(startIndex, endIndex); + List subListOfList = list.subList(startIndex, endIndex); + + assertTrue(subListOfBacking.equals(subListOfList)); + } + + @Test + public void toArray_sameItems_returnsTrue() { + final Integer[] arrayWithSameItems = new Integer[]{ITEM_ZERO, ITEM_ONE, ITEM_TWO, ITEM_THREE, ITEM_FOUR}; + final Object[] convertedToObjectArray = list.toArray(); + final Integer[] convertedToIntegerArray = list.toArray(new Integer[0]); + + assertTrue(Arrays.deepEquals(arrayWithSameItems, convertedToObjectArray)); + assertTrue(Arrays.equals(arrayWithSameItems, convertedToIntegerArray)); + } + + @Test + public void equals_symmetricList_returnsTrue() { + final UnmodifiableObservableList one = new UnmodifiableObservableList<>(FXCollections.observableList(backing)); + final UnmodifiableObservableList another = new UnmodifiableObservableList<>(FXCollections.observableList(backing)); + + assertTrue(one.equals(another) && another.equals(one)); + assertTrue(one.hashCode() == another.hashCode()); + } + + @Test + public void listIterator_iterateWholeList_listMatches() { + final ListIterator liter = list.listIterator(); + int currentItem; + int index; + + // cursor position 0 -> 1 (index 0) + assertTrue(liter.hasNext()); + + index = liter.nextIndex(); + assertEquals(index, 0); + + currentItem = liter.next(); + assertEquals(currentItem, ITEM_ZERO); + + // move cursor position 1 -> 2 -> 3 + liter.next(); + liter.next(); + + // cursor position 3 -> 2 (index 2) + assertTrue(liter.hasPrevious()); + + index = liter.previousIndex(); + assertEquals(index, 2); + + currentItem = liter.previous(); + assertEquals(currentItem, ITEM_TWO); + } +} +``` +###### /java/seedu/agendum/model/UserPrefsTest.java +``` java +public class UserPrefsTest { + + private UserPrefs one, another; + + @Before + public void setUp() { + one = new UserPrefs(); + another = new UserPrefs(); + } + + @Test + public void equals_differentObject_returnsFalse() { + assertFalse(one.equals(new Object())); + } + + @Test + public void equals_symmetric_returnsTrue() { + // equals to itself and object with same parameters + assertTrue(one.equals(one)); + assertTrue(one.equals(another)); + } + + @Test + public void hashcode_symmetric_returnsTrue() { + assertEquals(one.hashCode(), another.hashCode()); + } + + @Test + public void setGuiSettings_validInputs_successful() { + final double expectedWidth = 222; + final double expectedHeight = 333; + final int expectedX = 444; + final int expectedY = 555; + final GuiSettings expectedGuiSettings = new GuiSettings(expectedWidth, expectedHeight, expectedX, expectedY); + + final UserPrefs userPrefs = new UserPrefs(); + userPrefs.setGuiSettings(expectedWidth, expectedHeight, expectedX, expectedY); + GuiSettings actualGuiSettings = userPrefs.getGuiSettings(); + + assertEquals(actualGuiSettings, expectedGuiSettings); + } + +} +``` +###### /java/seedu/agendum/storage/StorageManagerTest.java +``` java + @Test + public void handleSaveLocationChangedEvent_validFilePath_success() { + String validPath = "data/test.xml"; + storageManager.handleChangeSaveLocationEvent(new ChangeSaveLocationEvent(validPath)); + assertEquals(storageManager.getToDoListFilePath(), validPath); + } + + @Test + public void handleLoadDataRequestEvent_validPathToFileInvalidFile_throwsException() throws IOException, FileDeletionException { + EventsCollector eventCollector = new EventsCollector(); + String validPath = "data/testLoad.xml"; + assert !FileUtil.isFileExists(validPath); + + // File does not exist + storageManager.handleLoadDataRequestEvent(new LoadDataRequestEvent(validPath)); + DataLoadingExceptionEvent dlee = (DataLoadingExceptionEvent)eventCollector.get(0); + assertTrue(dlee.exception instanceof NoSuchElementException); + + // File in wrong format + FileUtil.createFile(new File(validPath)); + storageManager.handleLoadDataRequestEvent(new LoadDataRequestEvent(validPath)); + dlee = (DataLoadingExceptionEvent)eventCollector.get(1); + assertTrue(dlee.exception instanceof DataConversionException); + FileUtil.deleteFile(validPath); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_nullPath_fail() { + // null + storageManager.setToDoListFilePath(null); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_pathEmpty_fail() { + // empty string + storageManager.setToDoListFilePath(""); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_pathInvalid_fail() { + // invalid file path + storageManager.setToDoListFilePath("1:/.xml"); + } + + public void setToDoListFilePath_pathValid_success() { + // valid file path + String validPath = "test/test.xml"; + storageManager.setToDoListFilePath(validPath); + assertEquals(validPath, storageManager.getToDoListFilePath()); + } +``` +###### /java/seedu/agendum/storage/XmlAdaptedTaskTest.java +``` java +public class XmlAdaptedTaskTest { + + private Optional optionalStartDateTime; + private Optional optionalEndDateTime; + + private XmlAdaptedTask xmlAdaptedTaskAllFields; + private XmlAdaptedTask xmlAdaptedTaskUncompleted; + private XmlAdaptedTask xmlAdaptedTaskNoStartDateTime; + private XmlAdaptedTask xmlAdaptedTaskNoEndDateTime; + + @Before + public void setUp() throws IllegalValueException { + LocalDate date = LocalDate.now(); + + LocalTime startTime = LocalTime.of(12, 0); // 12pm + LocalDateTime startDateTime = LocalDateTime.of(date, startTime); + optionalStartDateTime = Optional.of(startDateTime); + + LocalTime endTime = LocalTime.of(13, 0); // 1pm + LocalDateTime endDateTime = LocalDateTime.of(date, endTime); + optionalEndDateTime = Optional.of(endDateTime); + + Task taskWithAllFields = new Task(new Name("taskWithStartAndEndDateTimeCompleted"), optionalStartDateTime, optionalEndDateTime); + taskWithAllFields.markAsCompleted(); + xmlAdaptedTaskAllFields = new XmlAdaptedTask(taskWithAllFields); + + Task taskMarkedAsUncompleted = new Task(new Name("taskWithStartAndEndDateTimeUncompleted"), optionalStartDateTime, optionalEndDateTime); + xmlAdaptedTaskUncompleted = new XmlAdaptedTask(taskMarkedAsUncompleted); + + Task taskWithNoStartDateTime = new Task(new Name("taskWithNoStartDateTime"), Optional.ofNullable(null), optionalEndDateTime); + taskWithNoStartDateTime.markAsCompleted(); + xmlAdaptedTaskNoStartDateTime = new XmlAdaptedTask(taskWithNoStartDateTime); + + Task taskWithNoEndDateTime = new Task(new Name("taskWithNoEndDateTime"), optionalStartDateTime, Optional.ofNullable(null)); + taskWithNoEndDateTime.markAsCompleted(); + xmlAdaptedTaskNoEndDateTime = new XmlAdaptedTask(taskWithNoEndDateTime); + } + + public void assertTaskEqual(Task task, Optional startDateTime, Optional endDateTime, boolean isCompleted) { + assertTrue(task.getStartDateTime().equals(startDateTime)); + assertTrue(task.getEndDateTime().equals(endDateTime)); + assertTrue(task.isCompleted() == isCompleted); + } + + @Test + public void toModelType() throws IllegalValueException { + // Task with start date time, end date time, completed + Task taskWithAllFields = xmlAdaptedTaskAllFields.toModelType(); + assertTaskEqual(taskWithAllFields, optionalStartDateTime, optionalEndDateTime, true); + + // Task with start date time, end date time, not completed + Task taskMarkedAsUncompleted = xmlAdaptedTaskUncompleted.toModelType(); + assertTaskEqual(taskMarkedAsUncompleted, optionalStartDateTime, optionalEndDateTime, false); + + // Task with no start date time, is completed + Task taskWithNoStartDateTime = xmlAdaptedTaskNoStartDateTime.toModelType(); + assertTaskEqual(taskWithNoStartDateTime, Optional.empty(), optionalEndDateTime, true); + + // Task with no end date time, is completed + Task taskWithNoEndDateTime = xmlAdaptedTaskNoEndDateTime.toModelType(); + assertTaskEqual(taskWithNoEndDateTime, optionalStartDateTime, Optional.empty(), true); + } +} +``` +###### /java/seedu/agendum/storage/XmlToDoListStorageTest.java +``` java + @Test(expected = AssertionError.class) + public void setToDoListFilePath_nullPath_throwsAssertionError() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + xmlToDoListStorage.setToDoListFilePath(null); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_emptyPath_throwsAssertionError() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + // empty string + xmlToDoListStorage.setToDoListFilePath(""); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_invalidPath_throwsAssertionError() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + // invalid file path + xmlToDoListStorage.setToDoListFilePath("1:/.xml"); + } + + public void setToDoListFilePath_validPath_success() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + // valid file path + String validPath = "test/test.xml"; + xmlToDoListStorage.setToDoListFilePath(validPath); + assertEquals(validPath, xmlToDoListStorage.getToDoListFilePath()); + } + +} +``` diff --git a/config/checkstyle/checkstyle-noframes-sorted.xsl b/config/checkstyle/checkstyle-noframes-sorted.xsl deleted file mode 100644 index 9c0ac3054165..000000000000 --- a/config/checkstyle/checkstyle-noframes-sorted.xsl +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

CheckStyle Audit

Designed for use with CheckStyle and Ant.
-
- - - -
- - - -
- - - - -
- - - - -
- - - - -

Files

- - - - - - - - - - - - - - -
NameErrors
-
- - - - -

File

- - - - - - - - - - - - - - -
Error DescriptionLine
- Back to top -
- - - -

Summary

- - - - - - - - - - - - -
FilesErrors
-
- - - - a - b - - -
- - diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml deleted file mode 100644 index 3bab4e05bbae..000000000000 --- a/config/checkstyle/checkstyle.xml +++ /dev/null @@ -1,326 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/config/findbugs/excludeFilter.xml b/config/findbugs/excludeFilter.xml deleted file mode 100644 index 03c15ae4cc81..000000000000 --- a/config/findbugs/excludeFilter.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/copyright.txt b/copyright.txt deleted file mode 100644 index 93aa2a39ce25..000000000000 --- a/copyright.txt +++ /dev/null @@ -1,9 +0,0 @@ -Some code adapted from http://code.makery.ch/library/javafx-8-tutorial/ by Marco Jakob - -Copyright by Susumu Yoshida - http://www.mcdodesign.com/ -- address_book_32.png -- AddressApp.ico - -Copyright by Jan Jan Kovařík - http://glyphicons.com/ -- calendar.png -- edit.png diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 33df65bea583..7d49b740a432 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -4,49 +4,93 @@ We are a team based in the [School of Computing, National University of Singapor ## Project Team -#### [Damith C. Rajapakse](http://www.comp.nus.edu.sg/~damithch)
-
-**Role**: Project Advisor +#### [Muthu Kumar Chandrasekaran](https://www.quora.com/profile/Muthu-Kumar-Chandrasekaran)
+
+**Role: Project Advisor** ----- -#### [Joshua Lee](http://github.com/lejolly) -
-Role: Developer
-Responsibilities: UI +#### [Rachael Sim @rachx](https://github.com/rachx) +
+**Role: Team leader**
+* Components in charge of: Model
+* Aspects/tools in charge of: Scheduling and tracking, Code quality
+* Features implemented: + * [Delete multiple tasks](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#deleting-a-task--delete) + * [Mark multiple tasks](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#marking-a-task-as-completed--mark) + * [Unmark multiple tasks](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#unmarking-a-task--unmark) + * [Rename an existing task](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#renaming-a-task--rename) + * [Undo last changes to the task list](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#undoing-your-last-changes--undo) + * [Defining custom alias for commands](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#creating-an-alias-for-a-command--alias) + * [Removing custom alias for commands](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#removing-an-alias-command--unalias) +* Code written:
+[[Functional Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/main/A0133367E.md)]
+[[Testing Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/test/A0133367E.md)]
+[[Documentation](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/docs/A0133367E.md)]
+* Other major contributions: + * Did most of the refactoring from AddressBook to ToDoList + * Actively update user/developer guide ----- -#### [Leow Yijin](http://github.com/yijinl) -
-Role: Developer
-Responsibilities: Data +#### [Vishnu Prem @burnflare](http://github.com/burnflare) +
+**Role: Developer**
+* Components in charge of: Logic
+* Aspects/tools in charge of: Integration, Git
+* Features implemented: + * [Adding a task (with time)](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#adding-a-task-add) + * [Rescheduling a task](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#rescheduling-a-task--schedule) + * [Synchronizing with Google calendar](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#synchronizing-with-google-calendar-sync) + * Smart command correction +* Code written:
+[[Functional Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/main/A0003878Y.md)]
+[[Testing Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/test/A0003878Y.md)]
+[[Documentation](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/docs/A0003878Y.md)]
+* Other major contributions: + * Set up & manage CI (Travis, Coveralls and Codacy) ----- -#### [Martin Choo](http://github.com/m133225) -
-Role: Developer
-Responsibilities: Dev Ops +#### [Justin Tay @INCENDE](https://github.com/INCENDE) +
+**Role: Developer**
+* Components in charge of: Storage, Commons
+* Aspects/tools in charge of: Documentation, Deliverables and Deadlines
+* Features implemented: + * [Choose a custom storage location](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#specifying-the-data-storage-location--store) + * [Load from a specific storage location](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#loading-from-another-data-storage-location--load) +* Code written:
+[[Functional Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/main/A0148095X.md)]
+[[Testing Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/test/A0148095X.md)]
+[[Documentation](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/docs/A0148095X.md)]
+* Other major contributions: + * Actively update user/developer guide + * Correct most grammatical errors in user/developer guide + ----- -#### [Thien Nguyen](https://github.com/ndt93) - Role: Developer
- Responsibilities: Threading - - ----- +#### [Fan Weiguang @fanwgwg](https://github.com/fanwgwg) +
+**Role: Developer**
+* Components in charge of: UI, Main
+* Aspects/tools in charge of: Testing, Eclipse
+* Features implemented: + * Scrolling through previous command history + * [Tab to autocomplete](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#keyboard-shortcuts) + * [Help command](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#viewing-help--help) + * [Keyboard shortcuts](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/docs/UserGuide.md#keyboard-shortcuts) +* Code written:
+[[Functional Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/main/A0148031R.md)]
+[[Testing Code](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/test/A0148031R.md)]
+[[Documentation](https://github.com/CS2103AUG2016-W11-C2/main/blob/master/collated/docs/A0148031R.md)]
+* Other major contributions: + * Responsible for most of the UI + -#### [You Liang](http://github.com/yl-coder) -
- Role: Developer
- Responsibilities: UI - - ----- +----- # Contributors We welcome contributions. See [Contact Us](ContactUs.md) page for more info. - -* [Akshay Narayan](https://github.com/se-edu/addressbook-level4/pulls?q=is%3Apr+author%3Aokkhoy) -* [Sam Yong](https://github.com/se-edu/addressbook-level4/pulls?q=is%3Apr+author%3Amauris) \ No newline at end of file diff --git a/docs/ContactUs.md b/docs/ContactUs.md index 866d0de3fddc..97c951bd7a38 100644 --- a/docs/ContactUs.md +++ b/docs/ContactUs.md @@ -1,8 +1,8 @@ # Contact Us -* **Bug reports, Suggestions** : Post in our [issue tracker](https://github.com/se-edu/addressbook-level4/issues) +* **Bug reports, Suggestions** : Post in our [issue tracker](https://github.com/CS2103AUG2016-W11-C2/main/issues) if you noticed bugs or have suggestions on how to improve. * **Contributing** : We welcome pull requests. Follow the process described [here](https://github.com/oss-generic/process) -* **Email us** : You can also reach us at `damith [at] comp.nus.edu.sg` \ No newline at end of file +* **Email us** : You can also reach us at `sim.rachael [at] gmail.com` \ No newline at end of file diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 690b6d386627..393faa7cba7e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,5 +1,12 @@ -# Developer Guide +# Developer Guide + +  + + +## Table of contents + +* [Introduction](#introduction) * [Setting Up](#setting-up) * [Design](#design) * [Implementation](#implementation) @@ -9,291 +16,537 @@ * [Appendix B: Use Cases](#appendix-b--use-cases) * [Appendix C: Non Functional Requirements](#appendix-c--non-functional-requirements) * [Appendix D: Glossary](#appendix-d--glossary) -* [Appendix E : Product Survey](#appendix-e-product-survey) +* [Appendix E: Product Survey](#appendix-e--product-survey) + + +  + + +[comment]: # (@@author A0148031R) +## Introduction + +Agendum is a task manager for busy users to manage their schedules and tasks via keyboard commands. It is a Java desktop application that has a **GUI** implemented with JavaFX. + +This guide describes the design and implementation of Agendum. It will help developers (like you) understand how Agendum works and how to further contribute to its development. We have organized this guide in a top-down manner so that you can understand the big picture before moving on to the more detailed sections. Each sub-section is mostly self-contained to provide ease of reference. + + +  ## Setting up -#### Prerequisites +### Prerequisites -1. **JDK `1.8.0_60`** or later
+* **JDK `1.8.0_60`** or above
- > Having any Java 8 version is not enough.
- This app will not work with earlier versions of Java 8. - -2. **Eclipse** IDE -3. **e(fx)clipse** plugin for Eclipse (Do the steps 2 onwards given in + > This application will not work with any earlier versions of Java 8. + +* **Eclipse** IDE + +* **e(fx)clipse** plugin for Eclipse (Do the steps 2 onwards given in [this page](http://www.eclipse.org/efxclipse/install.html#for-the-ambitious)) -4. **Buildship Gradle Integration** plugin from the Eclipse Marketplace +* **Buildship Gradle Integration** plugin from the + [Eclipse Marketplace](https://marketplace.eclipse.org/content/buildship-gradle-integration) + + +### Importing the project into Eclipse -#### Importing the project into Eclipse +1. Fork this repo, and clone the fork to your computer -0. Fork this repo, and clone the fork to your computer -1. Open Eclipse (Note: Ensure you have installed the **e(fx)clipse** and **buildship** plugins as given - in the prerequisites above) -2. Click `File` > `Import` -3. Click `Gradle` > `Gradle Project` > `Next` > `Next` -4. Click `Browse`, then locate the project's directory -5. Click `Finish` +2. Open Eclipse (Note: Ensure you have installed the **e(fx)clipse** and **buildship** plugins as given in the prerequisites above) + +3. Click `File` > `Import` + +4. Click `Gradle` > `Gradle Project` > `Next` > `Next` + +5. Click `Browse`, then locate the project's directory + +6. Click `Finish` > * If you are asked whether to 'keep' or 'overwrite' config files, choose to 'keep'. > * Depending on your connection speed and server load, it can even take up to 30 minutes for the set up to finish - (This is because Gradle downloads library files from servers during the project set up process) - > * If Eclipse auto-changed any settings files during the import process, you can discard those changes. - -#### Troubleshooting project setup - -**Problem: Eclipse reports compile errors after new commits are pulled from Git** -* Reason: Eclipse fails to recognize new files that appeared due to the Git pull. -* Solution: Refresh the project in Eclipse:
- Right click on the project (in Eclipse package explorer), choose `Gradle` -> `Refresh Gradle Project`. - -**Problem: Eclipse reports some required libraries missing** -* Reason: Required libraries may not have been downloaded during the project import. -* Solution: [Run tests using Gardle](UsingGradle.md) once (to refresh the libraries). - + (Gradle needs time to download library files from servers during the project set up process) + > * If Eclipse automatically changed any settings during the import process, you can discard those changes. + + > After you are done importing Agendum, it will be a good practice to enable assertions before developing. This will enable Agendum app to verify assumptions along the way. To enable assertions, follow the instructions [here](http://stackoverflow.com/questions/5509082/eclipse-enable-assertions) + +### Troubleshooting project setup + +* **Problem: Eclipse reports compile errors after new commits are pulled from Git** + * Reason: Eclipse fails to recognize new files that appeared due to the Git pull. + * Solution: Refresh the project in Eclipse:
+ +* **Problem: Eclipse reports some required libraries missing** + * Reason: Required libraries may not have been downloaded during the project import. + * Solution: [Run tests using Gardle](UsingGradle.md) once (to refresh the libraries). + + +  + ## Design -### Architecture + +[comment]: # (@@author A0133367E) +### 1. Architecture
-The **_Architecture Diagram_** given above explains the high-level design of the App. -Given below is a quick overview of each component. - -`Main` has only one class called [`MainApp`](../src/main/java/seedu/address/MainApp.java). It is responsible for, -* At app launch: Initializes the components in the correct sequence, and connect them up with each other. -* At shut down: Shuts down the components and invoke cleanup method where necessary. - -[**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. -Two of those classes play important roles at the architecture level. -* `EventsCentre` : This class (written using [Google's Event Bus library](https://github.com/google/guava/wiki/EventBusExplained)) - is used by components to communicate with other components using events (i.e. a form of _Event Driven_ design) -* `LogsCenter` : Used by many classes to write log messages to the App's log file. - -The rest of the App consists four components. -* [**`UI`**](#ui-component) : The UI of tha App. -* [**`Logic`**](#logic-component) : The command executor. -* [**`Model`**](#model-component) : Holds the data of the App in-memory. -* [**`Storage`**](#storage-component) : Reads data from, and writes data to, the hard disk. - -Each of the four components -* Defines its _API_ in an `interface` with the same name as the Component. + +The **_Architecture Diagram_** given above summarizes the high-level design of Agendum. +Here is a quick overview of the main components of Agendum and their main responsibilities. + +#### `Main` +The **`Main`** component has a single class: [`MainApp`](../src/main/java/seedu/agendum/MainApp.java). It is responsible for initializing all components in the correct sequence and connecting them up with each other at app launch. It is also responsible for shutting down the other components and invoking the necessary clean up methods when Agendum is shut down. + + +#### `Commons` +[**`Commons`**](#6-common-classes) represents a collection of classes used by multiple other components. +Two Commons classes play important roles at the architecture level. + +* `EventsCentre` (written using [Google's Event Bus library](https://github.com/google/guava/wiki/EventBusExplained)) + is used by components to communicate with other components using events. +* `LogsCenter` is used by many classes to write log messages to Agendum's log file to record noteworthy system information and events. + + +#### `UI` +The [**`UI`**](#2-ui-component) component is responsible for interacting with the user by accepting commands, displaying data and results such as updates to the task list. + + +#### `Logic` +The [**`Logic`**](#3-logic-component) component is responsible for processing and executing the user's commands. + + +#### `Model` +The [**`Model`**](#4-model-component) component is responsible for representing and holding Agendum's data. + + +#### `Storage` +The [**`Storage`**](#5-storage-component) component is responsible for reading data from and writing data to the hard disk. + + +Each of the `UI`, `Logic`, `Model` and `Storage` components: + +* Defines its _API_ in an `interface` with the same name as the Component * Exposes its functionality using a `{Component Name}Manager` class. For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `LogicManager.java` class.

-The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the -command `delete 3`. - +#### Event Driven Approach +Agendum applies an Event-Driven approach and the **Observer Pattern** to reduce direct coupling between the various components. For example, the `UI` and `Storage` components are interested in receiving notifications when there is a change in the to-do list in `Model`. To avoid bidrectional coupling, `Model` does not inform these components of changes directly. Instead, it posts an event and rely on the `EventsCenter` to notifying the register Observers in `Storage` and `UI`. + +Consider the scenario where the user inputs `delete 1` described in the _Sequence Diagram_ below. The `UI` component will invoke the `Logic` component’s _execute_ method to carry out the given command, `delete 1`. The `Logic` component will identify the corresponding task and will call the `Model` component _deleteTasks_ method to update Agendum’s data and raise a `ToDoListChangedEvent`. + + + +The diagram below shows what happens after a `ToDoListChangedEvent` is raised. `EventsCenter` will inform its subscribers. `Storage` will respond and save the changes to hard disk while `UI` will respond and update the status bar to reflect the 'Last Updated' time.
->Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, - instead of asking the `Storage` to save the updates to the hard disk. + -The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates -being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time.
- +#### Model-View-Controller approach +To further reduce coupling between components, the Model-View-Controller pattern is also applied. The 3 components are as follows: +* Model: The `Model` component as previously described, maintains and holds Agendum's data. +* View: Part of the `UI` components and resources such as the .fxml file is responsible for displaying Agendum's data and interacting with the user. Through events, the `UI` component is able to get data updates from the model. +* Controller: Parts of the `UI` component such as (`CommandBox`) act as 'Controllers' for part of the UI. The `CommandBox` accepts user command input and request `Logic` to execute the command entered. This execution may result in changes in the model. -> Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having - to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct - coupling between components. -The sections below give more details of each component. +#### Activity Diagram -### UI component +
+ +The Activity Diagram above illustrates Agendum's workflow. Brown boxes represent actions taken by Agendum while orange boxes represent actions that involve interaction with the user. + +After Agendum is launched, Agendum will wait for the user to enter a command. Every command is parsed. If the command is valid and adheres to the given format, Agendum will executes the command. Agendum `Logic` component checks the input such as indices before updating the model and storage if needed. + +Agendum will then display changes in the to-do list and feedback of each command in the UI. The user can then enter a command again. Agendum will also give pop-up feedbacks when the command format or inputs are invalid. + +The following sections will then give more details of each individual component. + + +[comment]: # (@@author A0148031R) +### 2. UI component + +The `UI` is the entry point of Agendum which is responsible for showing updates to the user; changes in data in the `Model` automatically updates `UI` as well. `UI` executes user commands using the Logic Component. In addition, `UI` responds to events raised from various other parts of Agendum and updates the display accordingly.
-**API** : [`Ui.java`](../src/main/java/seedu/address/ui/Ui.java) +**API** : [`Ui.java`](../src/main/java/seedu/agendum/ui/Ui.java) + +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox` and `ResultPopup`. All these, including the `MainWindow`, inherit the abstract `UiPart` class. They can be loaded using `UiPartLoader`. -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, -`StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class -and they can be loaded using the `UiPartLoader`. +The `commandBox` component controls the field for user input, and it is associated with a `CommandBoxHistory` object which saves the most recent valid and invalid commands. `CommandBoxHistory` follows a singleton pattern to restrict the instantiation of the class to one object. -The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files - that are in the `src/main/resources/view` folder.
- For example, the layout of the [`MainWindow`](../src/main/java/seedu/address/ui/MainWindow.java) is specified in +Agendum has 3 different task panel classes `UpcomingTasksPanel`, `CompletedTaskPanel` and `FloatingTasksPanel`. They all inherit from the the `TaskPanel` class and hold and load `TaskCard` objects. + +The `UI` component uses JavaFX UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](../src/main/java/seedu/agendum/ui/MainWindow.java) is specified in [`MainWindow.fxml`](../src/main/resources/view/MainWindow.fxml) -The `UI` component, -* Executes user commands using the `Logic` component. -* Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. -* Responds to events raised from various parts of the App and updates the UI accordingly. -### Logic component +[comment]: # (@@author A0003878Y) +### 3. Logic component + +`Logic` provides several APIs for `UI` to execute the commands entered by the user. It also obtains information about the to-do list to render to the user. +The **API** of the logic component can be found at [`Logic.java`](../src/main/java/seedu/agendum/logic/Logic.java) + +The class diagram of the Logic Component is given below. `LogicManager` implements the `Logic Interface` and has exactly one `Parser`. `Parser` is responsible for processing the user command and creating instances of concrete `Command` objects (such as `AddCommand`) which will then be executed by the `LogicManager`. New command types must implement the `Command` class. Each `Command` class produces exactly one `CommandResult`.
-**API** : [`Logic.java`](../src/main/java/seedu/address/logic/Logic.java) +The `CommandLibrary` class is responsible for managing the various Agendum's reserved command keywords and their aliases. The `Parser` checks and queries the `CommandLibrary` to ascertain if a command word given has been aliased to a reserved command word. `AliasCommand` and `UnaliasCommand` will also check and update the `CommandLibrary` to add and remove aliases. The singleton pattern is applied to restrict the instantiation of the class to one object. This is to ensure that all other objects, such as `Parser`, `AliasCommand` and `UnaliasCommand` objects will refer to the same instance that records and manages all the alias relationships. + +You can view the Sequence Diagram below for interactions within the `Logic` component for the `execute("delete 1")` API call.
+ +
+ +#### Command Pattern +The Parser creates concrete Command objects such as `AddCommand` objects. `LogicManager` will then execute the various commands, such as `AddCommand` and `UndoCommand`. Each command does a different task and gives a different result. However, as all command types inherit from the abstract class `Command` and implement the _`execute`_ method, LogicManager (the invoker) can treat all of them as Command Object without knowing each specific Command type. By calling the _`execute`_ method, different actions result. + -1. `Logic` uses the `Parser` class to parse the user command. -2. This results in a `Command` object which is executed by the `LogicManager`. -3. The command execution can affect the `Model` (e.g. adding a person) and/or raise events. -4. The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. +[comment]: # (@@author A0133367E) +### 4. Model component -Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` - API call.
-
+As mentioned above, the `Model` component stores and manages Agendum's task list data and user's preferences. It also exposes a `UnmodifiableObservableList` that can be 'observed' by other components e.g. the `UI` can be bound to this list and will automatically update when the data in the list change. -### Model component +Due to the application of the **Observer Pattern**, it does not depend on other components such as `Storage` but interact by raising events instead. + +The `Model` class is the interface of the `Model` component. It provides several APIs for the `Logic` and `UI` components to update and retrieve Agendum’s task list data. The **API** of the model component can be found at [`Model.java`](../src/main/java/seedu/agendum/model/Model.java). + +The structure and relationship of the various classes in the `Model` component is described in the diagram below.
-**API** : [`Model.java`](../src/main/java/seedu/address/model/Model.java) +`ModelManager` implements the `Model` Interface. It contains a `UserPref` Object which represents the user’s preference and a `SyncManager` object which is necessary for the integration with Google calendar. + +`SyncManager` implements the `Sync` Interface. The `SyncManager` redirects the job of syncing a task to a `SyncProvider`. In Agendum, we have one provider, `SyncProviderGoogle` that implements the `SyncProvider` Interface. This is done so that it would be easy to extend Agendum to sync with other providers. One would just have to create a new class that extends the `SyncProvider` Interface and register that class with `SyncManager`. + +`ModelManager` contains a **main** `ToDoList` object and a stack of `ToDoList` objects referred to as `previousLists`. The **main** `ToDoList` object is the copy that is indirectly referred to by the `UI` and `Storage`. The stack, `previousLists` is used to support the [`undo` operation](#### undo). + +Each `ToDoList` object has one `UniqueTaskList` object. A `UniqueTaskList` can contain multiple `Task` objects but does not allow duplicates. + +The `ReadOnlyToDoList` and `ReadOnlyTask` interfaces allow other classes and components, such as the `UI`, to access but not modify the list of tasks and their details. + +Currently, each `Task` has a compulsory `Name` and last updated time. It is optional for a `Task` to have a start and end time. Each `Task` also has a completion status which is represented by a boolean. + +Design considerations: +> * `ToDoList` is a distinct class from `UniqueTaskList` as it can potentially be extended to have another `UniqueTagList` object to keep track of tags associated with each task and `ToDoList` will be responsible for syncing the tasks and tags. +> * `Name` is a separate class as it might be modified to have its own validation regex e.g. no / or " + +Using the same example, if the `Logic` component requests `Model` to _deleteTasks(task)_, the subsequent interactions between objects can be described by the following sequence diagram. + + + +The identified task is removed from the `UniqueTaskList`. The `ModelManager` raises a `ToDoListChangedEvent` and back up the current to do list to `previousLists` + +> `Model`’s _deleteTasks_ methods actually take in `ArrayList` instead of a single task. We use _deleteTasks(task)_ for simplicity in the sequence diagram. + + +#### undo + +`previousLists` is a Stack of `ToDoList` objects with a minimum size of 1. The `ToDoList` object at the top of the stack is identical to the **main** `ToDoList` object before any operation that mutate the to-do list is performed and after any operation that mutates the task list successfully (i.e. without exceptions). + +This is achieved with the _backupCurrentToDoList_ function which pushes a copy of the **main** `ToDoList` to the stack after any successful changes, such as the marking of multiple tasks. -The `Model`, -* stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes a `UnmodifiableObservableList` that can be 'observed' e.g. the UI can be bound to this list - so that the UI automatically updates when the data in the list change. -* does not depend on any of the other three components. +To undo the most recent changes, we simply pop the irrelevant `ToDoList` at the top of the `previousLists` stack and copy the `ToDoList` at the top of the stack back to the **main** list -### Storage component +This approach is reliable as it eliminates the need to implement an "undo" method and store the changes separately for each command that will mutate the task list. + +Also, it helps to resolve the complications involved with manipulating multiple task objects at a go. For example, the user might try to mark multiple tasks and one of which will result in a `DuplicateTaskException`. To revert the undesired changes to the **main** `ToDoList`, we can copy the the `ToDoList` at the top of the stack back to the **main** list. In such unsuccessful operations, the changes would not have persisted to Storage. + + +[comment]: # (@@author A0148095X) +### 5. Storage component
-**API** : [`Storage.java`](../src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](../src/main/java/seedu/agendum/storage/Storage.java) + +The `Storage` component has the following functions: + +* saves `UserPref` objects in json format and reads it back. +* saves the Agendum data in xml format and reads it back. + +Other components such as `Model` require the functionalities defined inside the `Storage` component in order to save task data and user preferences to the hard disk. The `Storage` component uses the *Facade* design pattern. Components such as `ModelManager` access storage sub-components through `StorageManager` which will then redirect method calls to its internal component such as `JsonUserPrefsStorage` and `XmlToDoListStorage`. `Storage` also shields the internal details of its components, such as the implementation of `XmlToDoListStorage`, from external classes. + +The Object Diagram below shows what it looks like during runtime. + +
+ +The Sequence Diagram below shows how the storage class will interact with model when `Load` command is executed. + +
+ + +### 6. Common classes + +Classes used by multiple components are in the `seedu.agendum.commons` package. + +They are further separated into sub-packages - namely `core`, `events`, `exceptions` and `util`. + +* Core - This package consists of the essential classes that are required by multiple components. +* Events -This package consists of the different type of events that can occur; these are used mainly by EventManager and EventBus. +* Exceptions - This package consists of exceptions that may occur with the use of Agendum. +* Util - This package consists of additional utilities for the different components. -The `Storage` component, -* can save `UserPref` objects in json format and read it back. -* can save the Address Book data in xml format and read it back. -### Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +  + +[comment]: # (@@author A0133367E) ## Implementation -### Logging +### 1. Logging We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. * The logging level can be controlled using the `logLevel` setting in the configuration file - (See [Configuration](#configuration)) + (See [Configuration](#2-configuration)) * The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level -* Currently log messages are output through: `Console` and to a `.log` file. +* Currently log messages are output through `Console` and to a `.log` file. **Logging Levels** -* `SEVERE` : Critical problem detected which may possibly cause the termination of the application -* `WARNING` : Can continue, but with caution -* `INFO` : Information showing the noteworthy actions by the App -* `FINE` : Details that is not usually noteworthy but may be useful in debugging - e.g. print the actual list instead of just its size +Currently, Agendum has 4 logging levels: `SEVERE`, `WARNING`, `INFO` and `FINE`. They record information pertaining to: + +* `SEVERE` : A critical problem which may cause the termination of Agendum
+ e.g. fatal error during the initialization of Agendum's main window +* `WARNING` : A problem which requires attention and caution but allows Agendum to continue working
+ e.g. error reading from/saving to config file +* `INFO` : Noteworthy actions by Agendum
+ e.g. valid and invalid commands executed and their results +* `FINE` : Less significant details that may be useful in debugging
+ e.g. all fine details of the tasks including their last updated time + +### 2. Configuration + +You can alter certain properties of our Agendum application (e.g. logging level) through the configuration file. +(default:`config.json`). -### Configuration -Certain properties of the application can be controlled (e.g App name, logging level) through the configuration file -(default: `config.json`): +  +[comment]: # (@@author A0148095X) ## Testing -Tests can be found in the `./src/test/java` folder. - -**In Eclipse**: -* To run all tests, right-click on the `src/test/java` folder and choose - `Run as` > `JUnit Test` -* To run a subset of tests, you can right-click on a test package, test class, or a test and choose - to run as a JUnit test. - -**Using Gradle**: -* See [UsingGradle.md](UsingGradle.md) for how to run tests using Gradle. - -We have two types of tests: - -1. **GUI Tests** - These are _System Tests_ that test the entire App by simulating user actions on the GUI. - These are in the `guitests` package. - -2. **Non-GUI Tests** - These are tests not involving the GUI. They include, - 1. _Unit tests_ targeting the lowest level methods/classes.
- e.g. `seedu.address.commons.UrlUtilTest` - 2. _Integration tests_ that are checking the integration of multiple code units - (those code units are assumed to be working).
- e.g. `seedu.address.storage.StorageManagerTest` - 3. Hybrids of unit and integration tests. These test are checking multiple code units as well as +You can find all the test files in the `./src/test/java` folder. + +### Types of Tests + +#### 1. GUI Tests + +These are _System Tests_ that test the entire App by simulating user actions on the GUI. +They are in the `guitests` package. + +#### 2. Non-GUI Tests + +These are tests that do not involve the GUI. They include, + * _Unit tests_ targeting the lowest level methods/classes.
+ e.g. `seedu.agendum.commons.StringUtilTest` tests the correctness of StringUtil methods e.g. if a source string contains a query string, ignoring letter cases. + * _Integration tests_ that are checking the integration of multiple code units + (individual code units are assumed to be working).
+ e.g. `seedu.agendum.storage.StorageManagerTest` tests if StorageManager is correctly connected to other storage components such as JsonUserPrefsStorage. + * Hybrids of _unit and integration tests_. These tests are checking multiple code units as well as how the are connected together.
- e.g. `seedu.address.logic.LogicManagerTest` - -**Headless GUI Testing** : + e.g. `seedu.agendum.logic.LogicManagerTest` will check various code units from the `Model` and `Logic` components. + +#### 3. Headless Mode GUI Tests + Thanks to the [TestFX](https://github.com/TestFX/TestFX) library we use, - our GUI tests can be run in the _headless_ mode. - In the headless mode, GUI tests do not show up on the screen. - That means the developer can do other things on the Computer while the tests are running.
- See [UsingGradle.md](UsingGradle.md#running-tests) to learn how to run tests in headless mode. - -#### Troubleshooting tests - **Problem: Tests fail because NullPointException when AssertionError is expected** - * Reason: Assertions are not enabled for JUnit tests. +our GUI tests can be run in [headless mode](#headless-mode).
+See [UsingGradle.md](UsingGradle.md#running-tests) for instructions on how to run tests in headless mode. + +### How to Test + +#### 1. Using Eclipse + +* To run all tests, right-click on the `src/test/java` folder and choose `Run as` > `JUnit Test` +* To run a subset of tests, you can right-click on a test package, test class, or a test and choose to run as a JUnit test. + +#### 2. Using Gradle + +* Launch a terminal on Mac or command window in Windows. Navigate to Agendum’s project directory. We recommend cleaning the project before running all tests in headless mode with the following command `./gradlew clean headless allTests` on Mac and `gradlew clean headless allTests` on Windows. +* See [UsingGradle.md](UsingGradle.md) for more details on how to run tests using Gradle. + +>#### Troubleshooting tests +>**Problem: Tests fail because NullPointException when AssertionError is expected** + +>* Reason: Assertions are not enabled for JUnit tests. This can happen if you are not using a recent Eclipse version (i.e. _Neon_ or later) - * Solution: Enable assertions in JUnit tests as described +>* Solution: Enable assertions in JUnit tests as described [here](http://stackoverflow.com/questions/2522897/eclipse-junit-ea-vm-option).
Delete run configurations created when you ran tests earlier. - + + +  + + +[comment]: # (@@author A0148031R) ## Dev Ops -### Build Automation +### 1. Build Automation -See [UsingGradle.md](UsingGradle.md) to learn how to use Gradle for build automation. +We use Gradle to run tests and manage library dependencies. The Gradle configuration for this project is defined in _build.gradle_. -### Continuous Integration +### 2. Continuous Integration -We use [Travis CI](https://travis-ci.org/) to perform _Continuous Integration_ on our projects. -See [UsingTravis.md](UsingTravis.md) for more details. +We use [Travis CI](https://travis-ci.org/) to perform _Continuous Integration_ on our project. When code is pushed to this repository, Travis CI will run the project tests automatically to ensure that existing functionality will not be negatively affected by the changes. -### Making a Release +### 3. Making a Release + +To contribute a new release: -Here are the steps to create a new release. - 1. Generate a JAR file [using Gradle](UsingGradle.md#creating-the-jar-file). - 2. Tag the repo with the version number. e.g. `v0.1` - 2. [Crete a new release using GitHub](https://help.github.com/articles/creating-releases/) - and upload the JAR file your created. - -### Managing Dependencies - -A project often depends on third-party libraries. For example, Address Book depends on the -[Jackson library](http://wiki.fasterxml.com/JacksonHome) for XML parsing. Managing these _dependencies_ -can be automated using Gradle. For example, Gradle can download the dependencies automatically, which -is better than these alternatives.
-a. Include those libraries in the repo (this bloats the repo size)
-b. Require developers to download those libraries manually (this creates extra work for developers)
+ 2. Tag the repo with the version number. e.g. `v1.1` + 2. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/) + and upload the JAR file you created. -## Appendix A : User Stories +### 4. Managing Dependencies -Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` +Agendum depends on third-party libraries, such as the +[Jackson library](http://wiki.fasterxml.com/JacksonHome), for XML parsing, [Natty](http://natty.joestelmach.com) for date & time parsing, [Reflection](https://code.google.com/archive/p/reflections/) for examining classes at runtime and [Google Calendar SDK](https://developers.google.com/api-client-library/java/apis/calendar/v3) for sync. Managing these dependencies have been automated using Gradle. Gradle can download the dependencies automatically hence the libraries are not included in this repo and you do not need to download these libraries manually. To add a new dependency, update `build.gradle`. +  + + +[comment]: # (@@author A0148095X) +## Appendix A : User Stories + +> Priorities: +> * High (must have) - `* * *` +> * Medium (nice to have) - `* *` +> * Low (unlikely to have) - `*` + Priority | As a ... | I want to ... | So that I can... -------- | :-------- | :--------- | :----------- -`* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App -`* * *` | user | add a new person | -`* * *` | user | delete a person | remove entries that I no longer need -`* * *` | user | find a person by name | locate details of persons without having to go through the entire list -`* *` | user | hide [private contact details](#private-contact-detail) by default | minimize chance of someone else seeing them by accident -`*` | user with many persons in the address book | sort persons by name | locate a person easily +`* * *` | User | See usage instructions | View more information about the features and commands available +`* * *` | User | Add a task | Keep track of tasks which I need work on +`* * *` | User | Add a task with start and end time | Keep track of events that need to be completed within a certain time-frame +`* * *` | User | Add a task with a deadline | Keep track of a task to be done by a specific date and time +`* * *` | User | Rename a task | update or enhance the description of a task +`* * *` | User | Edit or remove start and end time of tasks | Reschedule events with defined start and end dates +`* * *` | User | Edit or remove deadlines of tasks | Reschedule tasks which must be done by a certain date and time +`* * *` | User | Mark task(s) as completed | Keep record of tasks that have been completed without deleting, to distinguish between completed and uncompleted tasks +`* * *` | User | Unmark task(s) from completed | Update the status of my task(s) if there are new changes or I want to continue working on a recently completed task(s). +`* * *` | User | Delete task(s) | Remove task(s) that will never get done or are no longer relevant +`* * *` | User | Undo my last action(s) | Easily correct any accidental mistakes in the last command(s) +`* * *` | User | Search based on task name | Find a task without going through the entire list using a few key words. +`* * *` | User | View all my tasks | Return to the default view of task lists after I am done searching for tasks +`* * *` | User | Specify my data storage location | Easily relocate the raw file for editing and/or sync the file to a Cloud Storage service +`* * *` | User | Load from a file | Load Agendum’s task list from a certain location or a Cloud Storage service +`* * *` | User | Exit the application by typing a command | Close the app easily +`* *` | User | Filter overdue tasks and upcoming tasks (due within a week) | Decide on what needs to be done soon +`* *` | User | Filter tasks based on marked/unmarked | Review my completed tasks and decide on what I should do next +`* *` | User | Clear the command I am typing with a key | Enter a new command without having to backspace the entire command line +`* *` | Advanced user | Specify my own alias commands | Enter commands faster or change the name of a command to suit my needs +`* *` | Advanced user | Remove the alias for a command | Use it for another command alias +`* *` | Advanced user | Scroll through my past few commands | Check what I have done and redo actions easily +`* *` | Google calendar user | Sync my tasks from Agendum to Google calendar | Keep track of my tasks using both Agendum and Google Calendar +`*` | User | Add multiple time slots for a task | “Block” multiple time slots when the exact timing of a task is certain +`*` | User | Add tags for my tasks | Group tasks together and organise my task list +`*` | User | Search based on tags | Find all the tasks of a similar nature +`*` | User | Add/Remove tags for existing tasks | Update the grouping of tasks +`*` | User | Be notified of deadline/time clashes | Resolve these conflicts manually +`*` | User | Key in emojis/symbols and characters from other languages e.g. Mandarin | Capture information in other languages +`*` | User | Clear all existing tasks | Easily start afresh with a new task list +`*` | User | See the count/statistics for upcoming/ overdue and pending tasks | Know how many tasks I need to do +`*` | User | Sort tasks by alphabetical order and date | Organise and easily locate tasks +`*` | Advanced user | Import tasks from an existing text file | Add multiple tasks efficiently without relying on multiple commands +`*` | Advanced user | Save a backup of the application in a custom file | Restore it any time at a later date +`*` | Busy user | Add recurring events or tasks | Keep the same tasks in my task list without adding them manually +`*` | Busy User | Search for tasks by date (e.g. on/before a date) | Easily check my schedule and make plans accordingly +`*` | Busy User | Search for a time when I am free | Find a suitable slot to schedule an item +`*` | Busy user | Can specify a priority of a task | Keep track of what tasks are more important + + +  + + +[comment]: # (@@author A0148031R) +## Appendix B : Use Cases -{More to be added} +>For all use cases below, the **System** is `Agendum` and the **Actor** is the `user`, unless specified otherwise -## Appendix B : Use Cases +### Use case 01 - Add a task -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +**MSS** + +1. System prompts the Actor to enter a command +2. Actor enters an add command with the task name into the input box. +3. System adds the task. +4. System shows a feedback message ("Task `name` added") and displays the updated list + Use case ends. + +**Extensions** + +2a. No task description is provided + +> 2a1. System shows an error message (“Please provide a task name/description”)
+> Use case resumes at step 1 + +2b. There is an existing task with the same description and details + +> 2b1. System shows an error message (“Please use a new task description”)
+> Use case resumes at step 1 + +### Use case 02 - Delete a task + +**MSS** + +1. Actor requests to delete a specific task in the list by its index +2. System deletes the task. +3. System shows a success feedback message to describe the task deleted and displays the updated list + Use case ends. + +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends + +### Use case 03 - Rename a task + +**MSS** + +1. Actor requests to rename a specific task in the list by its index and also input the new task name +2. System updates the task +3. System shows a success feedback message to describe the task renamed and displays the updated list + Use case ends. + +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends + +1b. No task name is provided + +> 1b1. System shows an error message to inform the user of the incorrect format/missing name +> Use case ends + +2a. Renaming a task will result in a duplicate (will become exactly identical to another task) + +> 2a1. System shows an error message to inform user of potential duplicate
+> Use case ends -#### Use case: Delete person +### Use case 04 - Schedule a task’s start and end time/deadline **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person
-Use case ends. +1. Actor requests to list tasks +2. System shows a list of tasks +3. Actor inputs index and the new start/end time or deadline of the task to be modified +4. System updates the task +5. System shows a feedback message (“Task `index`'s time/date has been updated”) and displays the updated list +6. Use case ends. **Extensions** @@ -303,31 +556,284 @@ Use case ends. 3a. The given index is invalid -> 3a1. AddressBook shows an error message
- Use case resumes at step 2 +> 3a1. System shows an error message (“Please select a task on the list with a valid index”)
+> Use case resumes at step 2 -{More to be added} +3b. The new input time format is invalid -## Appendix C : Non Functional Requirements +> 3b1. System shows an error message (“Please follow the given time format”)
+> Use case resumes at step 2 + + +[comment]: # (@@author A0133367E) +### Use case 05 - Undo previous command that modified the task list + +**MSS** + +1. Actor requests to undo the last change to the task list. +2. System revert the last change to the task list. +3. System shows a success feedback message and displays the updated list. + Use case ends. + +**Extensions** + +2a. There are no previous modifications to the task list (since the launch of the application) + +> 2a1. System alerts the user that there are no previous changes
+> Use case ends + + +### Use case 06 - Mark a task as completed + +**MSS**: + +1. Actor requests to mark a task specified by its index in the list as completed +2. System marks the task as completed +3. System shows a success feedback message, updates and highlights the selected task. + Use case ends -1. Should work on any [mainstream OS](#mainstream-os) as long as it has Java `1.8.0_60` or higher installed. -2. Should be able to hold up to 1000 persons. -3. Should come with automated unit tests and open source code. -4. Should favor DOS style commands over Unix-style commands. +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends + +2a. Marking a task will result in a duplicate (will become exactly identical to an existing task) + +> 2a1. System shows an error message to inform user of potential duplicate
+> Use case ends + +### Use case 07 - Unmark a task + +**MSS**: + +1. Actor requests to unmark a task followed by its index +2. System unmarks the task from completed +3. System shows a success feedback message, updates and highlights the selected task. + Use case ends + +**Extensions** + +1a. The index given is invalid (e.g. it is a string or out of range) + +> 1a1. System shows an error message to inform the user of the incorrect format/index given +> Use case ends -{More to be added} +2a. Unmarking a task will result in a duplicate (will become exactly identical to an existing task) +> 2a1. System shows an error message to inform user of potential duplicate
+> Use case ends + + +[comment]: # (@@author A0148095X) +### Use case 08 - Add alias commands + +**MSS** + +1. Actor enters a alias command and specify the name and new alias name of the command +2. System alias the command +3. System shows a feedback message (“The command `original command` can now be keyed in as `alias key`”) +4. Use case ends. + +**Extensions** + +1a. There is no existing command with the original name specified + +> 1a1. System shows an error message (“There is no such existing command”)
+> Use case ends + +1b. The new alias name is already reserved/used for other commands + +> 1b1. System shows an error message ("The alias `alias key` is already in use")
+> Use case ends + + +### Use case 09 - Remove alias commands + +**MSS** + +1. Actor enters the unalias command followed by `alias key` +2. System removes the alias for the command +3. System shows a feedback message ("The alias `alias key` for `original command` has been removed.") +4. Use case ends. + +**Extensions** + +1a. There is no existing alias +> 1a1. System shows an error message (“There is no such existing alias”)
+> Use case ends + + +### Use case 10 - Specify data storage location + +**MSS** + +1. Actor enters store command followed by a path to file +2. System updates data storage location to the specified path to file +3. System saves task list to the new data storage location +4. System shows a feedback message ("New save location: `location`") +5. Use case ends. + +**Extensions** + +1a. Path to file is input as 'default' +> 1a1. System updates data storage location to default
+> 1a2. System shows a feedback message ("Save location set to default: `location`")
+> Use case ends + +1b. File exists +> 1b1. System shows an error message ("The specified file exists; would you like to use LOAD instead?")
+> Use case ends + +1c. Path to file is in the wrong format +> 1c1. System shows an error message ("The specified path is in the wrong format. Example: store agendum/todolist.xml")
+> Use case ends + +1d. Path to file is not accessible +> 1d1. System shows an error message ("The specified location is inaccessible; try running Agendum as administrator.")
+> Use case ends + + +### Use case 11 - Load from data file + +**MSS** + +1. Actor enters load command followed by a path to file +2. System saves current task list into existing data storage location +3. System loads task list from specified path to file +2. System updates data storage location to the specified path to file +3. System shows a feedback message ("Data successfully loaded from: `location`") +4. Use case ends. + +**Extensions** + +1a. Path to file is invalid +> 1a1. System shows an error message ("The specified path to file is invalid: `location`")
+> Use case ends + +2a. File does not exist +> 1a1. System shows an error message ("The specified file does not exist: `location`")
+> Use case ends + +3a. File is in the wrong format +> 3a1. System shows an error message ("File is in the wrong format.")
+> Use case ends + + +  + + +[comment]: # (@@author A0003878Y) +## Appendix C : Non Functional Requirements + +1. Should work on any [mainstream OS](#mainstream-os) as long as it has Java `1.8.0_60` or higher installed. +2. Should be able to hold up to 800 tasks in total (including completed tasks). +3. Should come with automated unit tests. +4. Should use a Continuous Integration server for real time status of master’s health. +5. Should be kept open source code. +6. Should favour DOS style commands over Unix-style commands. +7. Should adopt an object oriented design. +8. Should not violate any copyrights. +9. Should have a response time of less than 2 second for every action performed. +10. Should work offline without an internet connection. +11. Should work as a standalone application. +12. Should not use relational databases to store data. +13. Should store data in an editable text file. +14. Should not require an installer. +15. Should not use paid libraries and frameworks. +16. Should be a free software. +17. Should be easily transferrable between devices; only 1 folder needs to be transferred. +18. Should have documentation that matches the source code +19. Should not have unhandled exceptions from user input +20. Should be installable without assistance other than the user guide. +21. Should have understandable code such that new members can start working on the project within 1 week. + + +  + + +[comment]: # (@@author A0148095X) ## Appendix D : Glossary -##### Mainstream OS +##### Mainstream OS: -> Windows, Linux, Unix, OS-X +Windows, Linux, Unix, OS-X -##### Private contact detail +##### Headless Mode: -> A contact detail that is not meant to be shared with others +In the headless mode, GUI tests do not show up on the screen.
+This means you can do other things on the Computer while the tests are running. -## Appendix E : Product Survey -{TODO: Add a summary of competing products} +  + + +[comment]: # (@@author A0133367E) +## Appendix E : Product Survey +We conducted a product survey on other task managers. Here is a summary of the strengths and weaknesses of each application. The criteria used for evaluation are own preferences and Jim's requirements. + +#### Main insights +* Keyboard friendliness of our application is extremely important. It is useful to distinguish our application from the rest. Keyboard shortcuts must be intuitive, easy to learn and remember. + * Tab for autocomplete + * Scroll through command history or task list with up and down + * Allow users to specify their own shorthand commands so they will remember + * Summoning the help window with a keyboard shortcut +* Clear visual feedback on the status of the task + * Overdue and upcoming tasks should stand out + * Should also be able to see if a task is completed or recurring + * Identify if the task is selected/has just been updated +* Organized overview of all tasks + * Tasks should be sorted by their deadline/happening time + * Users might like to see their recently updated/completed tasks at the top of the list + * Allow user to see these various types of tasks and distinguish them without having to switch between lists (i.e. have multiple lists) +* Will be nice to allow more details for tasks + * detailed task descriptions + * tagging +* Commands should be intuitive and simple enough for new users + * more natural language like parsing for dates with prepositions as keywords + + +#### Wunderlist + +*Strengths:* + +* Clearly displays tasks that have not been completed +* Tasks can be categorized under different lists +* Tasks can have sub tasks +* Possible to highlight tasks by marking as important (starred) or pinning tasks +* Can set deadlines for tasks +* Can create recurring tasks +* Can associate files with tasks +* Can be used offline +* Keyboard friendly – keyboard shortcuts to mark tasks as completed and important +* Search and sort functionality makes finding and organizing tasks easier +* Possible to synchronize across devices +* Give notifications and reminders for tasks near deadline or overdue + +*Weaknesses:* + +* Wunderlist has a complex interface and might require multiple clicks to get specific tasks done. For example, it has separate field to add tasks, search for tasks and a sort button. There are various lists & sub-lists. Each list has a completed/uncompleted section and each task needs to be clicked to display the associated subtasks, notes, files and comment. +* New users might not know how to use the advanced features e.g. creating recurring tasks + +#### Google calendar + +*Strengths:* + +* Have a weekly/monthly/daily calendar view which will make it easy for users to visualize their schedules +* Can create recurring events +* Integrated with Gmail. A user can add events from emails easily and this is desirable since Jim's to do items arrive by emails +* Can be used offline +* Possible to synchronize across devices +* Calendar can be exported to CSV/iCal for other users +* CLI to quick add an event to a calendar instead of clicking through the screen +* Comprehensive search by name/details/people involved/location/time + + +*Weaknesses:* + +* Not possible to mark tasks as completed +* Not possible to add tasks without deadline or time +* CLI does not support updating of tasks/deleting etc. Still requires clicking. +* New users might not know of the keyboard shortcuts diff --git a/docs/LearningOutcomes.md b/docs/LearningOutcomes.md deleted file mode 100644 index 5ee57072a8d8..000000000000 --- a/docs/LearningOutcomes.md +++ /dev/null @@ -1,135 +0,0 @@ -# Learning Outcomes -After studying this code and completing the corresponding exercises, you should be able to, - -1. [Use High-Level Designs `[LO-HighLevelDesign]`](#use-high-level-designs-lo-highleveldesign) -1. [Use Event-Driven Programming `[LO-EventDriven]`](#use-event-driven-programming-lo-eventdriven`) -1. [Use API Design `[LO-ApiDesign]`](#use-api-design-lo-apidesign) -1. [Use Assertions `[LO-Assertions]`](#use-assertions-lo-assertions) -1. [Use Logging `[LO-Logging]`](#use-logging-lo-logging) -1. [Use Defensive Coding `[LO-DefensiveCoding]`](#use-defensive-coding-lo-defensivedoding) -1. [Use Build Automation `[LO-BuildAutomation]`](#use-build-automation-lo-buildautomation) -1. [Use Continuous Integration `[LO-ContinuousIntegration]`](#use-continuous-integration-lo-continuousintegration) - ------------------------------------------------------------------------------------------------------- - -## Use High-Level Designs `[LO-HighLevelDesign]` - -Note how the [Developer Guide](DeveloperGuide.md#architecture) describes the high-level design using an -_Architecture Diagrams_ and high-level sequence diagrams. - ------------------------------------------------------------------------------------------------------- - -## Use Event-Driven Programming `[LO-EventDriven]` - -Note how the [Developer Guide](DeveloperGuide.md#architecture) uses events to communicate with components -without needing a direct coupling. Also note how the `EventsCenter` class acts as an event dispatcher to -facilitate communication between event creators and event consumers. - ------------------------------------------------------------------------------------------------------- - -## Use API Design `[LO-ApiDesign]` - -Note how components of AddressBook have well-defined APIs. For example, the API of the `Logic` component -is given in the [`Logic.java`](../src/main/java/seedu/address/logic/Logic.java) -
- -**Resources** -* [A three-minutes video](https://www.youtube.com/watch?v=Un80XoRT1ME) of designing architecture of and - discovering component APIs for a Game of Tic-Tac-Toe. - ------------------------------------------------------------------------------------------------------- - -## Use Assertions `[LO-Assertions]` - -Note how the AddressBook app uses Java `assert`s to verify assumptions. - -**Resources** - * [Programming With Assertions](http://docs.oracle.com/javase/6/docs/technotes/guides/language/assert.html) - a - guide from Oracle. - * [How to enable assertions in Eclipse](http://stackoverflow.com/questions/5509082/eclipse-enable-assertions) - -#### Exercise: Add more assertions - * Make sure assertions are enabled in Eclipse by forcing an assertion failure (e.g. add `assert false;` somewhere in - the code and run the code to ensure the runtime reports an assertion failure). - - * Add more assertions to AddressBook as you see fit. - ------------------------------------------------------------------------------------------------------- - -## Use Logging `[LO-Logging]` - -Note [how the AddressBook app uses Java's `java.util.log` package to do logging](DeveloperGuide.md#logging). - -**Resources** - * Tutorials - * [Logging using java.util.logging](http://tutorials.jenkov.com/java-logging/index.html) - a tutorial by Jakob Jenkov - * [Logging tutorial](http://docs.oracle.com/javase/7/docs/technotes/guides/logging/overview.html) - a more detailed - tutorial from Oracle. - * Logging best practices - * [Apache Commons Logging guide](http://commons.apache.org/proper/commons-logging/guide.html#Message_PrioritiesLevels) - * [10 Tips for Proper Application Logging](https://www.javacodegeeks.com/2011/01/10-tips-proper-application-logging.html) - * [Base 22 Java Logging Standards and Guidelines](https://wiki.base22.com/display/btg/Java+Logging+Standards+and+Guidelines) - -#### Exercise: Add more logging - Add more logging to AddressBook as you see fit. - ------------------------------------------------------------------------------------------------------- - -## Use Defensive Coding `[LO-DefensiveCoding]` - - Note how AddressBook uses the `ReadOnly*` interfaces to prevent objects being modified by clients who are not - supposed to modify them. - -#### Exercise: identify more places for defensive coding - Analyze the AddressBook code/design to identify, - * where defensive coding is used - * where the code can be more defensive - ------------------------------------------------------------------------------------------------------- - -## Use Build Automation `[LO-BuildAutomation]` - -Note [how the AddressBook app uses Gradle to automate build tasks](UsingGradle.md). - -**Resources** - * Tutorials - * [Getting started with Gradle (Java)](https://gradle.org/getting-started-gradle-java/) - a tutorial from the Gradle team - * [Another tutorial](http://www.tutorialspoint.com/gradle/) - from TutorialPoint - -#### Exercise: Use gradle to run tasks - * Use gradle to do these tasks (instructions are [here](UsingGradle.md)) - : Run all tests in headless mode, build the jar file. - -#### Exercise: Use gradle to manage dependencies - * Note how the build script `build.gradle` file manages third party dependencies such as ControlsFx.
- Update that file to manage a third-party library dependency. - ------------------------------------------------------------------------------------------------------- - -## Use Continuous Integration `[LO-ContinuousIntegration]` - -Note [how the AddressBook app uses Travis to perform Continuous Integration](UsingTravis.md). - -**Resources** - * Tutorials - * [Getting started with Travis](https://docs.travis-ci.com/user/getting-started/) - a tutorial from the Travis team - -#### Exercise: Use Travis in your own project - * Set up Travis to perform CI on your own project. - ------------------------------------------------------------------------------------------------------- - -{More to be added} -* Integration testing -* System testing -* Acceptance testing (+dogfooding) -* Equivalence classes -* Boundary value analysis -* Test input combination -* GUI test automation -* Design patterns -* Static analysis -* Code reviews -* Code coverage - - diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 0cf4b84f7470..c97a65a12e6f 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,134 +1,590 @@ # User Guide - -* [Quick Start](#quick-start) +* [Introduction](#introduction) +* [Getting Started](#getting-started) + * [Download](#download) + * [Launch](#launch) + * [Visual Introduction](#visual-introduction) + * [Start using Agendum](#start-using-agendum) * [Features](#features) + * [Adding a task](#adding-a-task-add) + * [Renaming a task](#renaming-a-task--rename) + * [Rescheduling a task](#rescheduling-a-task--schedule) + * [Marking a task as completed](#marking-a-task-as-completed--mark) + * [Unmarking a task](#unmarking-a-task--unmark) + * [Deleting a task](#deleting-a-task--delete) + * [Undoing your last changes](#undoing-your-last-changes--undo) + * [Searching for tasks](#searching-for-tasks--find) + * [Listing all tasks](#listing-all-tasks--list) + * [Creating an alias for a command](#creating-an-alias-for-a-command--alias) + * [Removing an alias command](#removing-an-alias-command--unalias) + * [Viewing help](#viewing-help--help) + * [Specifying the data storage location](#specifying-the-data-storage-location--store) + * [Loading from another data storage location](#loading-from-another-data-storage-location--load) + * [Synchronizing with Google calendar](#synchronizing-with-google-calendar-sync) + * [Exiting Agendum](#exiting-agendum--exit) + * [Bonus: Keyboard shortcuts](#keyboard-shortcuts) * [FAQ](#faq) +* [Conclusion](#conclusion) * [Command Summary](#command-summary) + * [Date Time Format](#date-time-format) + +  + +[comment]: # (@@author A0148095X) +## Introduction +Hi there! Do you have too many tasks to do? Are you unable to keep track of all of them? Are you looking for a hassle-free task manager which will work swiftly? + +Enter Agendum. + +This task manager will assist you in completing all your tasks on time. It will automatically sort your tasks by date so you can always see the most urgent tasks at the top of the list! + +Agendum is simple, flexible and phenomenally keyboard friendly. With just one line of command, Agendum will carry out your wishes. You don’t ever have to worry about having to click multiple buttons and links. Agendum is even capable of supporting your own custom command words! This means that you can get things done even faster, your way. + + +  + + +[comment]: # (@@author A0003878Y) +## Getting Started + +### Download + +1. Ensure that you have Java version `1.8.0_60` or above installed on your computer. +2. Download the latest `Agendum.jar` from [here](../../../releases).
+
+3. Copy the jar file to the folder that you intend to use as the root directory of Agendum. + +### Launch + +To launch Agendum, double-click on `Agendum.jar` to launch Agendum. Welcome! + +Here is the main window you will be greeted with. Initially the task panels are empty but fill them up with tasks soon. + +
+ +[comment]: # (@@author A0133367E) +### Visual Introduction + +Here is what Agendum may look like with some tasks added and completed. + +
+ +Notice how Agendum has 3 panels: **"Do It Soon"**, **"Do It Anytime"** and **"Done"**. +* **"Do It Soon"** panel will show your **uncompleted** tasks with deadlines and events. Those tasks demand your attention at or by some specific time! Agendum has helpfully sorted these tasks by their deadline or event time. + * **Overdue** tasks _(e.g. tutorial)_ will stand out in red at the top of the list. + * **Upcoming** tasks (happening/due within a week) _(e.g. essay draft)_ will stand out in light green next. +* **"Do It Anytime"** panel will show your **uncompleted** tasks which you did not specify a deadline or happening time. Do these tasks anytime. +* **"Done"** panel will show all your completed tasks. To make it easier for you to keep track of what you have done recently, Agendum will always show the latest completed tasks at the top of the list. + +Agendum will clearly display the name and time associated with each task. Notice that each task is displayed with a ID. For example, the task *learn piano* has a ID *7* now. We will use this ID to refer to the task for some Agendum commands. + +The **Command Box** is located at the top of Agendum. Enter your keyboard commands into the box! +Just in case, there is a **Status Bar** located at the bottom of Agendum. You can check today's date and time, where your Agendum's to-do list data is located and when your data was last saved. + +Agendum also has a pretty **Help Window** which summarizes the commands you can use. Agendum might show pop-ups and highlights after each commands for you to review your changes. -## Quick Start +[comment]: # (@@author A0148031R) +### Start using Agendum +*This is a brief introduction and suggestion on how to get started with Agendum. Refer to our [Features](#features) section, for a more extensive coverage on what Agendum can do.* -0. Ensure you have Java version `1.8.0_60` or later installed in your Computer.
- > Having any Java 8 version is not enough.
- This app will not work with earlier versions of Java 8. - -1. Download the latest `addressbook.jar` from the [releases](../../../releases) tab. -2. Copy the file to the folder you want to use as the home folder for your Address Book. -3. Double-click the file to start the app. The GUI should appear in a few seconds. - > +**Step 1 - Get some help** -4. Type the command in the command box and press Enter to execute it.
- e.g. typing **`help`** and pressing Enter will open the help window. -5. Some example commands you can try: - * **`list`** : lists all contacts - * **`add`**` John Doe p/98765432 e/johnd@gmail.com a/John street, block 123, #01-01` : - adds a contact named `John Doe` to the Address Book. - * **`delete`**` 3` : deletes the 3rd contact shown in the current list - * **`exit`** : exits the app -6. Refer to the [Features](#features) section below for details of each command.
+Feeling lost or clueless? To see a summary of Agendum commands, use the keyboard shortcut CTRL + H to bring up the help screen as shown below. You can start typing a command and press ESC whenever you want to hide the help screen. +**Step 2 - Add a task** +Perhaps, you can start by adding a task to your empty Agendum to-do list. For example, you might remember you have to return your library books. Type the following line in the command box: + +`> add return library books` + +Since you did not specify a time to return the books, Agendum will add this task to the **Do It Anytime** panel. The task *return library books* has a ID *1* now. + +**Step 3 - Update your task (if needed)** + +You might change your mind and want to update the details of the task. For example, you might only want to return a single book "Animal Farm" instead. Type the following line in the command box: + +`> rename 1 return "Animal Farm"` + +Agendum will promptly update the changes. What if you suddenly discover the book is due within a week? You will want to return "Animal Farm" by Friday night. To (re)schedule the task, type the following command: + +`> schedule 1 by friday 8pm` + +Since you will have to return your books by a specific time, Agendum will move this task to the **Do It Soon** panel. + +**Step 4 - Mark a task as completed** + +With the help of Agendum, you remembered to return "Animal Farm" punctually on Friday. Record this by marking the task as completed. Type the following line in the command box: + +`> mark 1` + +Agendum will move the task _(return "Animal Farm")_ to the **Done** panel. + +**Step 5 - Good to go** + +Continue exploring Agendum. Add more tasks to your Agendum to-do list and try out the various convenient commands given in the next section. Do note that the ID of the task might change as new tasks are added, updated and marked. Agendum takes care of it for you but you should always refer to the current ID displayed. + +**Summary of all the visual changes** + +Here is a **summary of all the visual changes** you should see at every step: +
+ +From Step 4 to 5, the id of the task _return "Animal Farm"_ changed from 1 to 2. + + +  + + +[comment]: # (@@author A0003878Y) ## Features -> **Command Format** -> * Words in `UPPER_CASE` are the parameters. -> * Items in `SQUARE_BRACKETS` are optional. -> * Items with `...` after them can have multiple instances. -> * The order of parameters is fixed. - -#### Viewing help : `help` -Format: `help` - -> Help is also shown if you enter an incorrect command e.g. `abcd` - -#### Adding a person: `add` -Adds a person to the address book
-Format: `add NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` - -> Persons can have any number of tags (including 0) - -Examples: -* `add John Doe p/98765432 e/johnd@gmail.com a/John street, block 123, #01-01` -* `add Betsy Crowe p/1234567 e/betsycrowe@gmail.com a/Newgate Prison t/criminal t/friend` - -#### Listing all persons : `list` -Shows a list of all persons in the address book.
-Format: `list` - -#### Finding all persons containing any keyword in their name: `find` -Finds persons whose names contain any of the given keywords.
-Format: `find KEYWORD [MORE_KEYWORDS]` - -> * The search is case sensitive. e.g `hans` will not match `Hans` -> * The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -> * Only the name is searched. -> * Only full words will be matched e.g. `Han` will not match `Hans` -> * Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans` will match `Hans Bo` - -Examples: -* `find John`
- Returns `John Doe` but not `john` -* `find Betsy Tim John`
- Returns Any person having names `Betsy`, `Tim`, or `John` - -#### Deleting a person : `delete` -Deletes the specified person from the address book. Irreversible.
-Format: `delete INDEX` - -> Deletes the person at the specified `INDEX`. - The index refers to the index number shown in the most recent listing.
- The index **must be a positive integer** 1, 2, 3, ... - -Examples: -* `list`
- `delete 2`
- Deletes the 2nd person in the address book. -* `find Betsy`
- `delete 1`
- Deletes the 1st person in the results of the `find` command. - -#### Select a person : `select` -Selects the person identified by the index number used in the last person listing.
-Format: `select INDEX` - -> Selects the person and loads the Google search page the person at the specified `INDEX`. - The index refers to the index number shown in the most recent listing.
- The index **must be a positive integer** 1, 2, 3, ... - -Examples: -* `list`
- `select 2`
- Selects the 2nd person in the address book. -* `find Betsy`
- `select 1`
- Selects the 1st person in the results of the `find` command. - -#### Clearing all entries : `clear` -Clears all entries from the address book.
-Format: `clear` - -#### Exiting the program : `exit` -Exits the program.
-Format: `exit` - -#### Saving the data -Address book data are saved in the hard disk automatically after any command that changes the data.
-There is no need to save manually. +### Commands + +**Here are some general things to note:** +> * All command words are case-insensitive. e.g. `Add` will match `add` +> * Words enclosed in angle brackets, e.g.`` are the parameters. You can freely decide what you want to use in its place. +> * Parameters with `...` after them can have multiple instances (separated by whitespace). For example, `...` means that you can specify multiple indices such as `3 5 7`. + + +### Adding a task: `add` + +If you have a task to work on, add it to the Agendum to start keeping track!
+ +Here are the *acceptable format(s)*: + +* `add ` - adds a task which can be done anytime. +* `add by ` - adds a task which have to be done by the specified deadline. Note the keyword `by`. +* `add from to ` - adds a event which will take place between start time and end time. Note the keyword `from` and `to`. + +Here are some *examples*: + +``` +Description: I want to watch Star Wars but I don't have a preferred time. +> add watch Star Wars +Result: Agendum will add a task to the "Do It Anytime" panel. + +Description: I need to return my library books by the end of this week. +> add return library books by Friday 8pm +Result: Agendum will add a task "return library books" to the "Do It Soon" panel. +It will have a deadline set to the nearest upcoming Friday and with time 8pm. + +Description: I have a wedding dinner which will take place on 30 Oct night. +> add attend wedding dinner from 30 Oct 7pm to 30 Oct 9.30pm +Result: Agendum will add a task "attend wedding dinner" to the "Do It Soon" panel. +It will have a start time 30 Oct 7pm and end time 30 Oct 9.30pm. +``` + +> A task cannot have both a deadline and a event time. + +Did Agendum intepret part of your task name as a deadline/event time when you did not intend for it to do so? Simply `undo` the last command and remember to enclose your task name with **single** quotation mark this time around. +``` +> add 'drop by 7 eleven' by tmr +``` + +#### Date Time Format +How do you specify the ``, `` and `` of a task? + +Agendum supports a wide variety of date time formats. Combine any of the date format and time format below. The date/time formats are case insensitive too. + +*Date Format* + +| Date Format | Example(s) | +|-----------------|----------------------| +| Month/day | 1/23 | +| Day Month | 1 Oct | +| Month Day | Oct 1 | +| Day of the week | Wed, Wednesday | +| Relative date | today, tmr, next wed | + + > If no year is specified, it is always assumed to be the current year. + > It is possible to specify the year before or after the month-day pair in the first 3 formats (e.g. 1/23/2016 or 2016 1 Oct) + > The day of the week refers to the following week. For example, today is Sunday (30 Oct). Agendum will interpret Wednesday and Sunday as 2 Nov and 6 Nov respectively (a week from now). + +*Time Format* + +| Time Format | Example(s) | +|-----------------|-----------------------------------------| +| Hour | 10, 22 | +| Hour:Minute | 10:30 | +| Hour.Minute | 10.30 | +| Relative time | this morning, this afternoon, tonight | + +> By default, we use the 24 hour time format but we do support the meridian format as well e.g. 10am, 10pm + +Here are some examples of the results if these formats are used in conjunction with the `add` command. +``` +> add submit homework by 9pm +Result: The day is not specified. Agendum will create a task "submit homework" +with the deadline day as today (the date of creation) and time as 9pm + +> add use coupons by next Wed +Result: The time is not specified. Agendum will create a task "use coupons" +with deadline day as the upcoming Wednesday and time as the current time. + +> add attend wedding dinner from 10 Nov 8pm to 10 Nov 9pm +Result: All the date and time are specified and there is no ambiguity at all. +``` + +Note +> If no year, date or time is specified, the current year, date or time will be used. +> It is advisable to specify both the date and time. + + +Helpful tip: With Agendum, you can skip typing the second date only if the deadline/event is **happening some time in the future** +``` +> add attend wedding dinner from 30 Nov 8pm to 9pm +``` + + +[comment]: # (@@author A0133367E) +### Renaming a task : `rename` + +Agendum understands that plans and tasks change all the time.
+ +If you wish to update the description of a task, you can use the following *format*: + +* `rename ` - give a new name to the task identified by ``. The `` must be a positive number and be in the most recent to-do list displayed. + +Here is an *example*:
+
+ +``` +Description: I want to be more specific about the movie I want to watch for task id #2. +To update the name of the task, +> rename 2 watch Harry Potter +``` + +Agendum will promptly update the displayed task list!
+
+ + +### (Re)scheduling a task : `schedule` + +Agendum recognizes that your schedule might change, and therefore allows you to reschedule your tasks easily. + +Here are the *acceptable format(s)*: + +* `schedule ` - re-schedule the task identified by ``. It can now be done anytime. It is no longer bounded by a deadline or event time! +* `schedule by ` - set or update the deadline for the task identified. Note the keyword `by`. +* `schedule from to ` - update the start/end time of the task identified by ``. Note the keyword `from` and `to`. + +Note: + > * Again, `` must be a positive number and be in the most recent to-do list displayed. + > * ``, `` and `` must follow the format previously defined in [Date Time Format](#date-time-format) + > * A task cannot have both a deadline and a event time. + +Here are some *examples*:
+
+ +``` +Description: I decide that I can go for a run at any time instead. +> schedule 1 +Result: Agendum will start/end time of the task "go for a run" and it will +move to the "Do It Anytime" panel + +Description: I want to submit my reflection earlier. +> schedule 2 by tmr 2pm +Result: Agendum will update the deadline of "submit personal reflection". It +will then be sorted in the "Do It Soon" panel. +``` + +Agendum will promptly update the displayed task list!
+
+ + +### Marking a task as completed : `mark` + +Have you completed a task? Well done!
+Celebrate the achievement by recording this in Agendum. + +Here is the *format*: +* `mark ...` - mark all the tasks identified by ``(s) as completed. Each `` must be a positive number and in the most recent to-do list displayed. + +``` +Description: I just walked my dog! +> mark 4 +Result: Agendum will move "walk the dog" to the "Done" panel + +Description: I had a really productive day and did all the other tasks too. +> mark 1 2 3 +Result: Agendum will save you the hassle of marking each individual task as +completed one by one. It is satisfying to watch how all the tasks move to the +"Done" panel together. + +You can also try out any of the following examples: +> mark 1,2,3 +> mark 1-3 +The tasks with display ids 1, 2 and 3 will be marked as completed. +``` + +* You can specify a id (e.g. 1) or a range of id (e.g. 3-8). They must be separated by whitespace (e.g. 1 2 3) or commas (e.g. 2,3) + +The changes are as shown below.
+
+ + +### Unmarking a task : `unmark` + +You might change your mind and want to continue working on a recently completed task. +They will conveniently be located at the top of the done panel. + +To reflect the change in completion status in Agendum, here is the *format*: +* `unmark ...` - unmark all the tasks identified by ``s as completed. Each `` must be a positive number and in the most recent to-do list displayed. + +This works in the same way as the `mark` command. The tasks will then be moved to the **"Do It Soon"** or **"Do It Anytime"** panel accordingly.
+ + +### Deleting a task : `delete` + +We understand that there are some tasks which will never get done and are perhaps no longer relevant.
+You can remove these tasks from the task list to keep these tasks out of sight and out of mind. + +Here is the *format*: +* `delete ...` - delete all the tasks identified by ``s as completed. Each `` must be a positive number and in the most recent to-do list displayed. + +Here are some *examples*:
+
+ +``` +Description: I just walked my dog and no longer want to view this task anymore. +> delete 4 +Result: Agendum will delete the task "walk the dog" and it will no longer +appear in any of the 3 panels. + +Description: I do not want to view the tasks at all. +> delete 1 2 3 +Result: Agendum will save you the hassle of deleting each individual task but +still allows you to selectively choose what to delete. +You can also try out any of the following examples: +> delete 1,2,3 +> delete 1-3 +The tasks with display ids 1, 2 and 3 will be deleted. +``` + +* You can specify a id (e.g. 1) or a range of id (e.g. 3-8). They must be separated by whitespace (e.g. 1 2 3) or commas (e.g. 2,3) + +The deleted tasks will appear in a popup window.
+
+ + + +### Undoing your last changes : `undo` + +Agendum understands that you might make mistakes and change your mind. Hence, Agendum does offer some flexibility and allow you to reverse the effects of a few commands by simply typing `undo`. Multiple and successive `undo` are supported. + +Commands that can be "undone" include: +* `add` +* `rename` +* `schedule` +* `mark` +* `unmark` +* `delete` + +Although some commands cannot be undone, you can still reverse the effect manually and easily. +* `store` - choose to `store` in your previous location again +* `load` - choose to `load` data from your previous location +* `alias` - `unalias` the shorthand command you just defined +* `unalias` - `alias` the shorthand command you just removed +* `undo` - scroll through your previous commands using the and again and enter the command to execute it again +* `list`/`find` - there is only a change in your view but no change in the task data. To go back to the previous view, use ESC + +Examples: +``` +> add homework +Result: Agendum adds the task "homework" +> undo +Result: Agendum removes the task "homework" +``` + + +[comment]: # (@@author A0148031R) +### Searching for tasks : `find` + +As your task list grows over time, it may become harder to locate a task.
+Fortunately, Agendum can search and bring up these tasks to you (if only you remember some of the keywords):
+ +Here is the *format*: +* `find ...` - filter out all tasks containing any of the keyword(s) given + + > * The search is not case sensitive. e.g `assignment` will match `Assignment` + > * The order of the keywords does not matter. e.g. `2 essay` will match `essay 2` + > * Only the name is searched + > * Only full words will be matched e.g. `work` will not match `homework` + > * Tasks matching at least one keyword will be returned (i.e. `OR` search). e.g. If I search for `homework assignment`, I will get tasks with names that contains `homework` or `assignment` or both. + +Here is an *example*:
+
+ +Although you are looking at a narrowed down list of tasks, your data is not lost! Simply hit ESC to exit your find results and see a list of tasks. + + +### Listing all tasks : `list` + +Alternatively, after you are done searching for tasks, you can use the following command to return to the default view of all your tasks:
+The format is simply `list`. + + +[comment]: # (@@author A0148095X) +### Creating an alias for a command : `alias` + +Perhaps you want to type a command faster, or change the name of a command to suit your needs;
+fret not, Agendum allows you to define your own aliases for commands.
+You can use both the original command and your own shorthand alias to carry out the same action. + +To create an alias, here is the *format*: +* `alias ` + +> * `` must be a single alphanumeric word. It cannot be a original-command or already aliased to another command. +> * `` must be a command word that is specified in the Command Summary section + +Examples: +``` +> alias mark m +Result: you can now use `m` or `mark` to mark a task as completed. +> alias mark mk +Result: Now you can use "m", "mk" or "mark" to mark a task as completed. +``` + + +### Removing an alias command : `unalias` + +Is a current alias inconvenient? Have you thought of a better one?
+Or perhaps you are thinking of using an alias for another command instead.
+ +To remove a previously defined alias, here is the *format*: +* `unalias ` + +> * `` should be an alias you previously defined. +> * After removing this particular alias, you can still use the original command word or other unremoved aliases. + +Examples: +``` +If mark is aliased with "m" and "mk". +> unalias mk +Result: "mk" can no longer be used to mark tasks; now you can only use the +original command "mark" or "m" to mark a task as completed. +``` + + +[comment]: # (@@author A0148031R) +### Viewing help : `help` + +At any point in time, if you need some reminder about the commands available, you can use the `help` command. Type `help` or use Ctrl + H to summon the help screen. To exit the help screen, use Ctrl + H again, or simply press ESC. + +Here is a tip: You can directly enter your next command too! Agendum will also exit the help screen and show your task list. + + +[comment]: # (@@author A0148095X) +### Specifying the data storage location : `store` + +Are you considering moving Agendum’s data files to another file directory? +You might want to save your Agendum task list to a Cloud Storage service so you can easily access from another device. +Agendum offers you the flexibility in choosing where the task list data will be stored. +The task list data will be saved to the specific directory and future data will be saved in that location. + +Here is the *format*: +* `store ` + +> * `` must be a valid path to a file on the local computer. +> * If there is an existing file at ``, it will be overriden. +> * The data storage file at the original location will not be deleted. +> * This command is similar to a "Save as..." in other applications. + +Examples: +``` +> store C:/Dropbox/mytasklist.xml +``` + + +### Loading from another data storage location : `load` + +After relocating Agendum’s data files, you might want to load that exact copy of Agendum’s task list from a certain location, or from a Cloud Storage service. Agendum also offers you the flexibility to choose which data files to import. + +Here is the *format*: +* `load ` + +> * `` must be a valid path to a file on the local computer. +> * Your current data would have already been saved automatically in its original data storage location. +> * Agendum will then show data loaded from `` and save data there in the future. +> * You will not be able to `undo` immediately after loading as there have been no changes to the loaded list. + +Examples +``` +> load C:/Dropbox/mytasklist.xml +``` + +### Synchronizing with Google calendar: `sync` + +If you have a Google account and want to synchronize your tasks from Agendum to Google Calendar, this command enables you to do exactly that! Synchronization takes place when you turn it on. + +Here is the *format*: +* `sync ON` or `sync OFF` + +> * `sync` must have either ON or OFF after the command word +> * Only data from Agendum will be synchronized to Google Calendar +> * Only tasks with a start and end date/time will be synchronized +> * Please accept or decline Google's request for permission for Agendum client to manage your calendar. Do not close the window abruptly. + + +### Exiting Agendum : `exit` + +Are you done with organizing your tasks? Well done!
+To leave Agendum, type `exit`. See you soon! + + +### Keyboard Shortcuts + +To work even faster you can also use keyboard shortcuts:
+1. Use and to scroll through previously typed commands. You don't need to remember or enter them again!
+2. If you are entering a new command, use to instantly clear the command line and start afresh.
+3. Use Tab to quickly auto-complete a command word when you are typing.
+4. Use Ctrl + H to conveniently switch between the help window and the command box. Also, you can use ESC to close help window.
+5. Use Ctrl + Z in place of `undo` + + +  + ## FAQ -**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with - the file that contains the data of your previous Address Book folder. - + +
+
Q: How do I save my task data in Agendum?
+
Agendum saves your data automatically whenever your task list is updated. There is no need to save manually. Agendum will save the data at the speicified storage location. By default, it will save to `data/todolist.xml`
+ +
Q: How do I transfer my data to another computer?
+
Firstly, note down the current save location of Agendum's task data (which is displayed in the bottom status bar). In your file directory, navigate to this location and copy the data file to a portable USB device, hard disk or your cloud storage folder. Alternatively, you can make use of the store command to transfer the file within Agendum. Then, ensure that you have installed Agendum in the other computer. Copy the data file from your device onto the other computer, preferrably in the same folder as Agendum. Use the load command to load it into Agendum.
+ +
Q: Why did Agendum complain about an invalid file directory?
+
Check if the directory you wish to relocate to exists and if you have enough administrator privileges.
+ +
Q: Can Agendum remind me when my task is due soon?
+
Agendum will always show the tasks that are due soon at the top of list. However, Agendum will not show you a reminder (yet).
+ +
Q: Why did Agendum complain that the task already exists?
+
You have previously created a task with the same name, start and end time. The tasks have the same completion status too! Save the trouble of creating one or it will be helpful to distinguish them by renaming instead. + +
Q: Why did Agendum reject my alias for a command?
+
The short-hand command cannot be one of Agendum’s command keywords (e.g. add, delete) and cannot be concurrently used to alias another command (e.g. m cannot be used for both mark and unmark).
+ +
Q: I can't launch Agendum. What is wrong?
+
Check if the config file in data/json/config.json contains the correct file paths to other data such as your to-do list. It might be helpful to delete the user preferences file.
+ +
+ + +  + + +[comment]: # (@@author A0133367E) +## Conclusion +We hope that you will find Agendum and our user guide helpful. If you have any suggestions on how we can make Agendum better or improve this guide, please feel free to post on our [issue tracker](https://github.com/CS2103AUG2016-W11-C2/main/issues). + + ## Command Summary -Command | Format --------- | :-------- -Add | `add NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` -Clear | `clear` -Delete | `delete INDEX` -Find | `find KEYWORD [MORE_KEYWORDS]` -List | `list` -Help | `help` -Select | `select INDEX` +
+ +For a quick reference, +> * Parameters with `...` after them can have multiple instances (separated by whitespace). +> * Commands are case insensitive +> * ``, `` and `` must follow the format previously defined in [Date Time Format](#date-time-format) diff --git a/docs/UsingGradle.md b/docs/UsingGradle.md index 578c5f8634c2..2d65c6cb09ee 100644 --- a/docs/UsingGradle.md +++ b/docs/UsingGradle.md @@ -82,4 +82,4 @@ relevant Gradle tasks. Checks whether the project has the required dependencies to perform testing, and download any missing dependencies before compiling the test classes.
See `build.gradle` -> `allprojects` -> `dependencies` -> `testCompile` for the list of - dependencies required. + dependencies required. \ No newline at end of file diff --git a/docs/UsingTravis.md b/docs/UsingTravis.md deleted file mode 100644 index 4844f0682f75..000000000000 --- a/docs/UsingTravis.md +++ /dev/null @@ -1,47 +0,0 @@ -# Travis CI - -[Travis CI](https://travis-ci.org/) is a _Continuous Integration_ platform for GitHub projects. - -Travis CI can run the projects' tests automatically whenever new code is pushed to the repo. -This ensures that existing functionality and features have not been broken by the changes. - -The current Travis CI set up performs the following things whenever someone push code to the repo: - * Runs the `./gradlew clean headless allTests coverage coveralls -i` command - (see [UsingGradle.md](UsingGradle.md) for more details on what this command means). - * Automatically retries the build up to 3 times if a task fails. - -If you would like to customise your travis build further, you can learn more about Travis -from [Travis CI Documentation](https://docs.travis-ci.com/). - -## Setting up Travis CI - -1. Fork the repo to your own organization. -2. Go to https://travis-ci.org/ and click `Sign in with GitHub`, then enter your GitHub account details if needed.
-![Signing into Travis CI](images/signing_in.png) - -3. Head to the [Accounts](https://travis-ci.org/profile) page, and find the switch for the forked repository. - * If the organization is not shown, click `Review and add` as shown below:
- ![Review and add](images/review_and_add.png)
- This should bring you to a GitHub page that manages the access of third-party applications. - Depending on whether you are the owner of the repository, you can either grant access - ![Grant Access](images/grant_access.png)
- or request access
- ![Request Access](images/request_access.png)
- to Travis CI so that it can access your commits and build your code. - - * If repository cannot be found, click `Sync account` -4. Activate the switch.
- ![Activate the switch](images/flick_repository_switch.png) -5. This repo comes with a [`.travis.yml`](.travis.yml) that tells Travis what to do. - So there is no need for you to create one yourself. -6. To see the CI in action, push a commit to the master branch! - * Go to the repository and see the pushed commit. There should be an icon which will link you to the Travis build.
- ![Commit build](images/build_pending.png) - - * As the build is run on a provided remote machine, we can only examine the logs it produces:
- ![Travis build](images/travis_build.png) - -7. If the build is successful, you should be able to check the coverage details of the tests - at [Coveralls](http://coveralls.io/) -8. Update the link to the 'build status' badge at the top of the `README.md` to point to the build status of your - own repo. \ No newline at end of file diff --git a/docs/diagrams/Diagrams.pptx b/docs/diagrams/Diagrams.pptx index 3c28abe9c1d3..c82b6cb6a5ea 100644 Binary files a/docs/diagrams/Diagrams.pptx and b/docs/diagrams/Diagrams.pptx differ diff --git a/docs/images/Architecture.png b/docs/images/Architecture.png index bdc789000f77..589da2f5ddc6 100644 Binary files a/docs/images/Architecture.png and b/docs/images/Architecture.png differ diff --git a/docs/images/DamithRajapakse.jpg b/docs/images/DamithRajapakse.jpg deleted file mode 100644 index 127543883893..000000000000 Binary files a/docs/images/DamithRajapakse.jpg and /dev/null differ diff --git a/docs/images/DeletePersonSdForLogic.png b/docs/images/DeletePersonSdForLogic.png deleted file mode 100644 index 6c272fb17af6..000000000000 Binary files a/docs/images/DeletePersonSdForLogic.png and /dev/null differ diff --git a/docs/images/DeleteTaskSdForLogic.png b/docs/images/DeleteTaskSdForLogic.png new file mode 100644 index 000000000000..ddb92ff69587 Binary files /dev/null and b/docs/images/DeleteTaskSdForLogic.png differ diff --git a/docs/images/JoshuaLee.jpg b/docs/images/JoshuaLee.jpg deleted file mode 100644 index 2d1d94e0cf5d..000000000000 Binary files a/docs/images/JoshuaLee.jpg and /dev/null differ diff --git a/docs/images/LeowYijin.jpg b/docs/images/LeowYijin.jpg deleted file mode 100644 index adbf62ad9406..000000000000 Binary files a/docs/images/LeowYijin.jpg and /dev/null differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index a973d02047a2..c60f7882469a 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/MartinChoo.jpg b/docs/images/MartinChoo.jpg deleted file mode 100644 index fd14fb94593a..000000000000 Binary files a/docs/images/MartinChoo.jpg and /dev/null differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 8cdf11ec93a1..87caeed260a3 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/SDforDeletePerson.png b/docs/images/SDforDeletePerson.png deleted file mode 100644 index 1e836f10dcd8..000000000000 Binary files a/docs/images/SDforDeletePerson.png and /dev/null differ diff --git a/docs/images/SDforDeletePersonEventHandling.png b/docs/images/SDforDeletePersonEventHandling.png deleted file mode 100644 index ecec0805d32c..000000000000 Binary files a/docs/images/SDforDeletePersonEventHandling.png and /dev/null differ diff --git a/docs/images/SDforDeleteTask.png b/docs/images/SDforDeleteTask.png new file mode 100644 index 000000000000..5a67a3f94640 Binary files /dev/null and b/docs/images/SDforDeleteTask.png differ diff --git a/docs/images/SDforDeleteTaskEventHandling.png b/docs/images/SDforDeleteTaskEventHandling.png new file mode 100644 index 000000000000..54bb9403253d Binary files /dev/null and b/docs/images/SDforDeleteTaskEventHandling.png differ diff --git a/docs/images/SDforDeleteTaskModelComponent.png b/docs/images/SDforDeleteTaskModelComponent.png new file mode 100644 index 000000000000..53f506c045ad Binary files /dev/null and b/docs/images/SDforDeleteTaskModelComponent.png differ diff --git a/docs/images/SDforLoad.png b/docs/images/SDforLoad.png new file mode 100644 index 000000000000..75dfec6e7602 Binary files /dev/null and b/docs/images/SDforLoad.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 7a4cd2700cbf..d6d871a1bdd6 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/StorageManagerObjectDiagram.png b/docs/images/StorageManagerObjectDiagram.png new file mode 100644 index 000000000000..4594a9892565 Binary files /dev/null and b/docs/images/StorageManagerObjectDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 7121a50a442a..ba4f811add29 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 459245e267af..f05b3b92592f 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiMarkTask.png b/docs/images/UiMarkTask.png new file mode 100644 index 000000000000..c1875f619e46 Binary files /dev/null and b/docs/images/UiMarkTask.png differ diff --git a/docs/images/UiScreenshot.png b/docs/images/UiScreenshot.png new file mode 100644 index 000000000000..3ddfa39613d5 Binary files /dev/null and b/docs/images/UiScreenshot.png differ diff --git a/docs/images/YouLiang.jpg b/docs/images/YouLiang.jpg deleted file mode 100644 index 17b48a732272..000000000000 Binary files a/docs/images/YouLiang.jpg and /dev/null differ diff --git a/docs/images/activityDiagram.jpg b/docs/images/activityDiagram.jpg new file mode 100644 index 000000000000..fe4ea4b9481d Binary files /dev/null and b/docs/images/activityDiagram.jpg differ diff --git a/docs/images/activityDiagram2.jpg b/docs/images/activityDiagram2.jpg new file mode 100644 index 000000000000..bfa943bf54a4 Binary files /dev/null and b/docs/images/activityDiagram2.jpg differ diff --git a/docs/images/build_pending.png b/docs/images/build_pending.png deleted file mode 100644 index 2e2f0d4380ca..000000000000 Binary files a/docs/images/build_pending.png and /dev/null differ diff --git a/docs/images/flick_repository_switch.png b/docs/images/flick_repository_switch.png deleted file mode 100644 index a6009dd44cdb..000000000000 Binary files a/docs/images/flick_repository_switch.png and /dev/null differ diff --git a/docs/images/grant_access.png b/docs/images/grant_access.png deleted file mode 100644 index beb4c0ddc8b0..000000000000 Binary files a/docs/images/grant_access.png and /dev/null differ diff --git a/docs/images/profiles/Justin.png b/docs/images/profiles/Justin.png new file mode 100644 index 000000000000..cd848704eab3 Binary files /dev/null and b/docs/images/profiles/Justin.png differ diff --git a/docs/images/profiles/Rachael.png b/docs/images/profiles/Rachael.png new file mode 100644 index 000000000000..55e637d4cb02 Binary files /dev/null and b/docs/images/profiles/Rachael.png differ diff --git a/docs/images/profiles/Vishnu.png b/docs/images/profiles/Vishnu.png new file mode 100644 index 000000000000..7cffacbe48c7 Binary files /dev/null and b/docs/images/profiles/Vishnu.png differ diff --git a/docs/images/profiles/Weiguang.png b/docs/images/profiles/Weiguang.png new file mode 100644 index 000000000000..243c3939e0ec Binary files /dev/null and b/docs/images/profiles/Weiguang.png differ diff --git a/docs/images/request_access.png b/docs/images/request_access.png deleted file mode 100644 index 12e8a81bd28f..000000000000 Binary files a/docs/images/request_access.png and /dev/null differ diff --git a/docs/images/review_and_add.png b/docs/images/review_and_add.png deleted file mode 100644 index 81d60a36e885..000000000000 Binary files a/docs/images/review_and_add.png and /dev/null differ diff --git a/docs/images/signing_in.png b/docs/images/signing_in.png deleted file mode 100644 index 6d1ad4fe26f3..000000000000 Binary files a/docs/images/signing_in.png and /dev/null differ diff --git a/docs/images/travis_build.png b/docs/images/travis_build.png deleted file mode 100644 index 0c4061bc0e23..000000000000 Binary files a/docs/images/travis_build.png and /dev/null differ diff --git a/docs/images/userguide/DownloadAgendum.png b/docs/images/userguide/DownloadAgendum.png new file mode 100644 index 000000000000..1df0e65ffa67 Binary files /dev/null and b/docs/images/userguide/DownloadAgendum.png differ diff --git a/docs/images/userguide/OpenAgendum.png b/docs/images/userguide/OpenAgendum.png new file mode 100644 index 000000000000..f1fb8d55b541 Binary files /dev/null and b/docs/images/userguide/OpenAgendum.png differ diff --git a/docs/images/userguide/afterDeleting.png b/docs/images/userguide/afterDeleting.png new file mode 100644 index 000000000000..a86534898115 Binary files /dev/null and b/docs/images/userguide/afterDeleting.png differ diff --git a/docs/images/userguide/afterRenaming.png b/docs/images/userguide/afterRenaming.png new file mode 100644 index 000000000000..5679e9ad634b Binary files /dev/null and b/docs/images/userguide/afterRenaming.png differ diff --git a/docs/images/userguide/afterScheduling.png b/docs/images/userguide/afterScheduling.png new file mode 100644 index 000000000000..39b84c5516c4 Binary files /dev/null and b/docs/images/userguide/afterScheduling.png differ diff --git a/docs/images/userguide/beforeDeleting.png b/docs/images/userguide/beforeDeleting.png new file mode 100644 index 000000000000..6658e975c9f2 Binary files /dev/null and b/docs/images/userguide/beforeDeleting.png differ diff --git a/docs/images/userguide/beforeRenaming.png b/docs/images/userguide/beforeRenaming.png new file mode 100644 index 000000000000..02b7715c77a8 Binary files /dev/null and b/docs/images/userguide/beforeRenaming.png differ diff --git a/docs/images/userguide/beforeScheduling.png b/docs/images/userguide/beforeScheduling.png new file mode 100644 index 000000000000..e6119943a55a Binary files /dev/null and b/docs/images/userguide/beforeScheduling.png differ diff --git a/docs/images/userguide/commandBox.png b/docs/images/userguide/commandBox.png new file mode 100644 index 000000000000..36e25ae3325b Binary files /dev/null and b/docs/images/userguide/commandBox.png differ diff --git a/docs/images/userguide/commandsummary.png b/docs/images/userguide/commandsummary.png new file mode 100644 index 000000000000..f37163dec063 Binary files /dev/null and b/docs/images/userguide/commandsummary.png differ diff --git a/docs/images/userguide/findResult.png b/docs/images/userguide/findResult.png new file mode 100644 index 000000000000..d761e3806ace Binary files /dev/null and b/docs/images/userguide/findResult.png differ diff --git a/docs/images/userguide/launch.png b/docs/images/userguide/launch.png new file mode 100644 index 000000000000..d72d29ab9dea Binary files /dev/null and b/docs/images/userguide/launch.png differ diff --git a/docs/images/userguide/mainui.png b/docs/images/userguide/mainui.png new file mode 100644 index 000000000000..628f90ed1343 Binary files /dev/null and b/docs/images/userguide/mainui.png differ diff --git a/docs/images/userguide/markMultiple.png b/docs/images/userguide/markMultiple.png new file mode 100644 index 000000000000..a40d12c53095 Binary files /dev/null and b/docs/images/userguide/markMultiple.png differ diff --git a/docs/images/userguide/marking.png b/docs/images/userguide/marking.png new file mode 100644 index 000000000000..2eafd0b60e13 Binary files /dev/null and b/docs/images/userguide/marking.png differ diff --git a/docs/images/userguide/releases.png b/docs/images/userguide/releases.png new file mode 100644 index 000000000000..fcb67c0acc50 Binary files /dev/null and b/docs/images/userguide/releases.png differ diff --git a/docs/images/userguide/startAgendum.png b/docs/images/userguide/startAgendum.png new file mode 100644 index 000000000000..f256e825a1a1 Binary files /dev/null and b/docs/images/userguide/startAgendum.png differ diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 728342109ca9..000000000000 --- a/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.daemon=true -org.gradle.parallel=false -org.gradle.jvmargs=-XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8 diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/address/commons/core/Config.java deleted file mode 100644 index 6441c9ef20f4..000000000000 --- a/src/main/java/seedu/address/commons/core/Config.java +++ /dev/null @@ -1,99 +0,0 @@ -package seedu.address.commons.core; - -import java.util.Objects; -import java.util.logging.Level; - -/** - * Config values used by the app - */ -public class Config { - - public static final String DEFAULT_CONFIG_FILE = "config.json"; - - // Config values customizable through config file - private String appTitle = "Address App"; - private Level logLevel = Level.INFO; - private String userPrefsFilePath = "preferences.json"; - private String addressBookFilePath = "data/addressbook.xml"; - private String addressBookName = "MyAddressBook"; - - - public Config() { - } - - public String getAppTitle() { - return appTitle; - } - - public void setAppTitle(String appTitle) { - this.appTitle = appTitle; - } - - public Level getLogLevel() { - return logLevel; - } - - public void setLogLevel(Level logLevel) { - this.logLevel = logLevel; - } - - public String getUserPrefsFilePath() { - return userPrefsFilePath; - } - - public void setUserPrefsFilePath(String userPrefsFilePath) { - this.userPrefsFilePath = userPrefsFilePath; - } - - public String getAddressBookFilePath() { - return addressBookFilePath; - } - - public void setAddressBookFilePath(String addressBookFilePath) { - this.addressBookFilePath = addressBookFilePath; - } - - public String getAddressBookName() { - return addressBookName; - } - - public void setAddressBookName(String addressBookName) { - this.addressBookName = addressBookName; - } - - - @Override - public boolean equals(Object other) { - if (other == this){ - return true; - } - if (!(other instanceof Config)){ //this handles null as well. - return false; - } - - Config o = (Config)other; - - return Objects.equals(appTitle, o.appTitle) - && Objects.equals(logLevel, o.logLevel) - && Objects.equals(userPrefsFilePath, o.userPrefsFilePath) - && Objects.equals(addressBookFilePath, o.addressBookFilePath) - && Objects.equals(addressBookName, o.addressBookName); - } - - @Override - public int hashCode() { - return Objects.hash(appTitle, logLevel, userPrefsFilePath, addressBookFilePath, addressBookName); - } - - @Override - public String toString(){ - StringBuilder sb = new StringBuilder(); - sb.append("App title : " + appTitle); - sb.append("\nCurrent log level : " + logLevel); - sb.append("\nPreference file Location : " + userPrefsFilePath); - sb.append("\nLocal data file location : " + addressBookFilePath); - sb.append("\nAddressBook name : " + addressBookName); - return sb.toString(); - } - -} diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java deleted file mode 100644 index 1deb3a1e4695..000000000000 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ /dev/null @@ -1,13 +0,0 @@ -package seedu.address.commons.core; - -/** - * Container for user visible messages. - */ -public class Messages { - - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; - public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - -} diff --git a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java deleted file mode 100644 index 347a8359e0d5..000000000000 --- a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package seedu.address.commons.events.model; - -import seedu.address.commons.events.BaseEvent; -import seedu.address.model.ReadOnlyAddressBook; - -/** Indicates the AddressBook in the model has changed*/ -public class AddressBookChangedEvent extends BaseEvent { - - public final ReadOnlyAddressBook data; - - public AddressBookChangedEvent(ReadOnlyAddressBook data){ - this.data = data; - } - - @Override - public String toString() { - return "number of persons " + data.getPersonList().size() + ", number of tags " + data.getTagList().size(); - } -} diff --git a/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java deleted file mode 100644 index 0580d27aecf5..000000000000 --- a/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.address.commons.events.ui; - -import seedu.address.commons.events.BaseEvent; - -/** - * Indicates a request to jump to the list of persons - */ -public class JumpToListRequestEvent extends BaseEvent { - - public final int targetIndex; - - public JumpToListRequestEvent(int targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - -} diff --git a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java deleted file mode 100644 index 95377b326fa6..000000000000 --- a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java +++ /dev/null @@ -1,26 +0,0 @@ -package seedu.address.commons.events.ui; - -import seedu.address.commons.events.BaseEvent; -import seedu.address.model.person.ReadOnlyPerson; - -/** - * Represents a selection change in the Person List Panel - */ -public class PersonPanelSelectionChangedEvent extends BaseEvent { - - - private final ReadOnlyPerson newSelection; - - public PersonPanelSelectionChangedEvent(ReadOnlyPerson newSelection){ - this.newSelection = newSelection; - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - public ReadOnlyPerson getNewSelection() { - return newSelection; - } -} diff --git a/src/main/java/seedu/address/commons/util/UrlUtil.java b/src/main/java/seedu/address/commons/util/UrlUtil.java deleted file mode 100644 index 6bbab52b9840..000000000000 --- a/src/main/java/seedu/address/commons/util/UrlUtil.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.commons.util; - -import java.net.URL; - -/** - * A utility class for URL - */ -public class UrlUtil { - - /** - * Returns true if both URLs have the same base URL - */ - public static boolean compareBaseUrls(URL url1, URL url2) { - - if (url1 == null || url2 == null) { - return false; - } - return url1.getHost().toLowerCase().replaceFirst("www.", "") - .equals(url2.getHost().replaceFirst("www.", "").toLowerCase()) - && url1.getPath().replaceAll("/", "").toLowerCase() - .equals(url2.getPath().replaceAll("/", "").toLowerCase()); - } - -} diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java deleted file mode 100644 index ce4dc1903cff..000000000000 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ /dev/null @@ -1,41 +0,0 @@ -package seedu.address.logic; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.ComponentManager; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.parser.Parser; -import seedu.address.model.Model; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.storage.Storage; - -import java.util.logging.Logger; - -/** - * The main LogicManager of the app. - */ -public class LogicManager extends ComponentManager implements Logic { - private final Logger logger = LogsCenter.getLogger(LogicManager.class); - - private final Model model; - private final Parser parser; - - public LogicManager(Model model, Storage storage) { - this.model = model; - this.parser = new Parser(); - } - - @Override - public CommandResult execute(String commandText) { - logger.info("----------------[USER COMMAND][" + commandText + "]"); - Command command = parser.parseCommand(commandText); - command.setData(model); - return command.execute(); - } - - @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); - } -} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index 2860a9ab2a85..000000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.logic.commands; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.*; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; - -import java.util.HashSet; -import java.util.Set; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: NAME p/PHONE e/EMAIL a/ADDRESS [t/TAG]...\n" - + "Example: " + COMMAND_WORD - + " John Doe p/98765432 e/johnd@gmail.com a/311, Clementi Ave 2, #02-25 t/friends t/owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Convenience constructor using raw values. - * - * @throws IllegalValueException if any of the raw values are invalid - */ - public AddCommand(String name, String phone, String email, String address, Set tags) - throws IllegalValueException { - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(new Tag(tagName)); - } - this.toAdd = new Person( - new Name(name), - new Phone(phone), - new Email(email), - new Address(address), - new UniqueTagList(tagSet) - ); - } - - @Override - public CommandResult execute() { - assert model != null; - try { - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } catch (UniquePersonList.DuplicatePersonException e) { - return new CommandResult(MESSAGE_DUPLICATE_PERSON); - } - - } - -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 522d57189f51..000000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,22 +0,0 @@ -package seedu.address.logic.commands; - -import seedu.address.model.AddressBook; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - public ClearCommand() {} - - - @Override - public CommandResult execute() { - assert model != null; - model.resetData(AddressBook.getEmptyAddressBook()); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java deleted file mode 100644 index f46f2f31353e..000000000000 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ /dev/null @@ -1,15 +0,0 @@ -package seedu.address.logic.commands; - -/** - * Represents the result of a command execution. - */ -public class CommandResult { - - public final String feedbackToUser; - - public CommandResult(String feedbackToUser) { - assert feedbackToUser != null; - this.feedbackToUser = feedbackToUser; - } - -} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index 1bfebe8912a8..000000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,50 +0,0 @@ -package seedu.address.logic.commands; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.UnmodifiableObservableList; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList.PersonNotFoundException; - -/** - * Deletes a person identified using it's last displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the last person listing.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - public final int targetIndex; - - public DeleteCommand(int targetIndex) { - this.targetIndex = targetIndex; - } - - - @Override - public CommandResult execute() { - - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); - - if (lastShownList.size() < targetIndex) { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - ReadOnlyPerson personToDelete = lastShownList.get(targetIndex - 1); - - try { - model.deletePerson(personToDelete); - } catch (PersonNotFoundException pnfe) { - assert false : "The target person cannot be missing"; - } - - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - -} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java deleted file mode 100644 index d98233ce2a0b..000000000000 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package seedu.address.logic.commands; - -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.events.ui.ExitAppRequestEvent; - -/** - * Terminates the program. - */ -public class ExitCommand extends Command { - - public static final String COMMAND_WORD = "exit"; - - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; - - public ExitCommand() {} - - @Override - public CommandResult execute() { - EventsCenter.getInstance().post(new ExitAppRequestEvent()); - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT); - } - -} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index 1d61bf6cc857..000000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -package seedu.address.logic.commands; - -import java.util.Set; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case sensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-sensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final Set keywords; - - public FindCommand(Set keywords) { - this.keywords = keywords; - } - - @Override - public CommandResult execute() { - model.updateFilteredPersonList(keywords); - return new CommandResult(getMessageForPersonListShownSummary(model.getFilteredPersonList().size())); - } - -} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java deleted file mode 100644 index 65af96940242..000000000000 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ /dev/null @@ -1,26 +0,0 @@ -package seedu.address.logic.commands; - - -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.events.ui.ShowHelpRequestEvent; - -/** - * Format full help instructions for every command for display. - */ -public class HelpCommand extends Command { - - public static final String COMMAND_WORD = "help"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" - + "Example: " + COMMAND_WORD; - - public static final String SHOWING_HELP_MESSAGE = "Opened help window."; - - public HelpCommand() {} - - @Override - public CommandResult execute() { - EventsCenter.getInstance().post(new ShowHelpRequestEvent()); - return new CommandResult(SHOWING_HELP_MESSAGE); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 9bdd457a1b01..000000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,20 +0,0 @@ -package seedu.address.logic.commands; - - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - public ListCommand() {} - - @Override - public CommandResult execute() { - model.updateFilteredListToShowAll(); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java deleted file mode 100644 index 9ca0551f1951..000000000000 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ /dev/null @@ -1,44 +0,0 @@ -package seedu.address.logic.commands; - -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.core.Messages; -import seedu.address.commons.events.ui.JumpToListRequestEvent; -import seedu.address.commons.core.UnmodifiableObservableList; -import seedu.address.model.person.ReadOnlyPerson; - -/** - * Selects a person identified using it's last displayed index from the address book. - */ -public class SelectCommand extends Command { - - public final int targetIndex; - - public static final String COMMAND_WORD = "select"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Selects the person identified by the index number used in the last person listing.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s"; - - public SelectCommand(int targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute() { - - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); - - if (lastShownList.size() < targetIndex) { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex - 1)); - return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex)); - - } - -} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java deleted file mode 100644 index 959b2cd0383c..000000000000 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ /dev/null @@ -1,192 +0,0 @@ -package seedu.address.logic.parser; - -import seedu.address.logic.commands.*; -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.exceptions.IllegalValueException; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -/** - * Parses user input. - */ -public class Parser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - private static final Pattern PERSON_INDEX_ARGS_FORMAT = Pattern.compile("(?.+)"); - - private static final Pattern KEYWORDS_ARGS_FORMAT = - Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace - - private static final Pattern PERSON_DATA_ARGS_FORMAT = // '/' forward slashes are reserved for delimiter prefixes - Pattern.compile("(?[^/]+)" - + " (?p?)p/(?[^/]+)" - + " (?p?)e/(?[^/]+)" - + " (?p?)a/(?
[^/]+)" - + "(?(?: t/[^/]+)*)"); // variable number of tags - - public Parser() {} - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - */ - public Command parseCommand(String userInput) { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return prepareAdd(arguments); - - case SelectCommand.COMMAND_WORD: - return prepareSelect(arguments); - - case DeleteCommand.COMMAND_WORD: - return prepareDelete(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return prepareFind(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - default: - return new IncorrectCommand(MESSAGE_UNKNOWN_COMMAND); - } - } - - /** - * Parses arguments in the context of the add person command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareAdd(String args){ - final Matcher matcher = PERSON_DATA_ARGS_FORMAT.matcher(args.trim()); - // Validate arg string format - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - try { - return new AddCommand( - matcher.group("name"), - matcher.group("phone"), - matcher.group("email"), - matcher.group("address"), - getTagsFromArgs(matcher.group("tagArguments")) - ); - } catch (IllegalValueException ive) { - return new IncorrectCommand(ive.getMessage()); - } - } - - /** - * Extracts the new person's tags from the add command's tag arguments string. - * Merges duplicate tag strings. - */ - private static Set getTagsFromArgs(String tagArguments) throws IllegalValueException { - // no tags - if (tagArguments.isEmpty()) { - return Collections.emptySet(); - } - // replace first delimiter prefix, then split - final Collection tagStrings = Arrays.asList(tagArguments.replaceFirst(" t/", "").split(" t/")); - return new HashSet<>(tagStrings); - } - - /** - * Parses arguments in the context of the delete person command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareDelete(String args) { - - Optional index = parseIndex(args); - if(!index.isPresent()){ - return new IncorrectCommand( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); - } - - return new DeleteCommand(index.get()); - } - - /** - * Parses arguments in the context of the select person command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareSelect(String args) { - Optional index = parseIndex(args); - if(!index.isPresent()){ - return new IncorrectCommand( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE)); - } - - return new SelectCommand(index.get()); - } - - /** - * Returns the specified index in the {@code command} IF a positive unsigned integer is given as the index. - * Returns an {@code Optional.empty()} otherwise. - */ - private Optional parseIndex(String command) { - final Matcher matcher = PERSON_INDEX_ARGS_FORMAT.matcher(command.trim()); - if (!matcher.matches()) { - return Optional.empty(); - } - - String index = matcher.group("targetIndex"); - if(!StringUtil.isUnsignedInteger(index)){ - return Optional.empty(); - } - return Optional.of(Integer.parseInt(index)); - - } - - /** - * Parses arguments in the context of the find person command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareFind(String args) { - final Matcher matcher = KEYWORDS_ARGS_FORMAT.matcher(args.trim()); - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, - FindCommand.MESSAGE_USAGE)); - } - - // keywords delimited by whitespace - final String[] keywords = matcher.group("keywords").split("\\s+"); - final Set keywordSet = new HashSet<>(Arrays.asList(keywords)); - return new FindCommand(keywordSet); - } - -} \ No newline at end of file diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 298cc1b82ce8..000000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,163 +0,0 @@ -package seedu.address.model; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; - -import java.util.*; -import java.util.stream.Collectors; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .equals comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - private final UniqueTagList tags; - - { - persons = new UniquePersonList(); - tags = new UniqueTagList(); - } - - public AddressBook() {} - - /** - * Persons and Tags are copied into this addressbook - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(toBeCopied.getUniquePersonList(), toBeCopied.getUniqueTagList()); - } - - /** - * Persons and Tags are copied into this addressbook - */ - public AddressBook(UniquePersonList persons, UniqueTagList tags) { - resetData(persons.getInternalList(), tags.getInternalList()); - } - - public static ReadOnlyAddressBook getEmptyAddressBook() { - return new AddressBook(); - } - -//// list overwrite operations - - public ObservableList getPersons() { - return persons.getInternalList(); - } - - public void setPersons(List persons) { - this.persons.getInternalList().setAll(persons); - } - - public void setTags(Collection tags) { - this.tags.getInternalList().setAll(tags); - } - - public void resetData(Collection newPersons, Collection newTags) { - setPersons(newPersons.stream().map(Person::new).collect(Collectors.toList())); - setTags(newTags); - } - - public void resetData(ReadOnlyAddressBook newData) { - resetData(newData.getPersonList(), newData.getTagList()); - } - -//// person-level operations - - /** - * Adds a person to the address book. - * Also checks the new person's tags and updates {@link #tags} with any new tags found, - * and updates the Tag objects in the person to point to those in {@link #tags}. - * - * @throws UniquePersonList.DuplicatePersonException if an equivalent person already exists. - */ - public void addPerson(Person p) throws UniquePersonList.DuplicatePersonException { - syncTagsWithMasterList(p); - persons.add(p); - } - - /** - * Ensures that every tag in this person: - * - exists in the master list {@link #tags} - * - points to a Tag object in the master list - */ - private void syncTagsWithMasterList(Person person) { - final UniqueTagList personTags = person.getTags(); - tags.mergeFrom(personTags); - - // Create map with values = tag object references in the master list - final Map masterTagObjects = new HashMap<>(); - for (Tag tag : tags) { - masterTagObjects.put(tag, tag); - } - - // Rebuild the list of person tags using references from the master list - final Set commonTagReferences = new HashSet<>(); - for (Tag tag : personTags) { - commonTagReferences.add(masterTagObjects.get(tag)); - } - person.setTags(new UniqueTagList(commonTagReferences)); - } - - public boolean removePerson(ReadOnlyPerson key) throws UniquePersonList.PersonNotFoundException { - if (persons.remove(key)) { - return true; - } else { - throw new UniquePersonList.PersonNotFoundException(); - } - } - -//// tag-level operations - - public void addTag(Tag t) throws UniqueTagList.DuplicateTagException { - tags.add(t); - } - -//// util methods - - @Override - public String toString() { - return persons.getInternalList().size() + " persons, " + tags.getInternalList().size() + " tags"; - // TODO: refine later - } - - @Override - public List getPersonList() { - return Collections.unmodifiableList(persons.getInternalList()); - } - - @Override - public List getTagList() { - return Collections.unmodifiableList(tags.getInternalList()); - } - - @Override - public UniquePersonList getUniquePersonList() { - return this.persons; - } - - @Override - public UniqueTagList getUniqueTagList() { - return this.tags; - } - - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && this.persons.equals(((AddressBook) other).persons) - && this.tags.equals(((AddressBook) other).tags)); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(persons, tags); - } -} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java deleted file mode 100644 index d14a27a93b5e..000000000000 --- a/src/main/java/seedu/address/model/Model.java +++ /dev/null @@ -1,35 +0,0 @@ -package seedu.address.model; - -import seedu.address.commons.core.UnmodifiableObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; - -import java.util.Set; - -/** - * The API of the Model component. - */ -public interface Model { - /** Clears existing backing model and replaces with the provided new data. */ - void resetData(ReadOnlyAddressBook newData); - - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - - /** Deletes the given person. */ - void deletePerson(ReadOnlyPerson target) throws UniquePersonList.PersonNotFoundException; - - /** Adds the given person */ - void addPerson(Person person) throws UniquePersonList.DuplicatePersonException; - - /** Returns the filtered person list as an {@code UnmodifiableObservableList} */ - UnmodifiableObservableList getFilteredPersonList(); - - /** Updates the filter of the filtered person list to show all persons */ - void updateFilteredListToShowAll(); - - /** Updates the filter of the filtered person list to filter by the given keywords*/ - void updateFilteredPersonList(Set keywords); - -} diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java deleted file mode 100644 index 869226d02bf1..000000000000 --- a/src/main/java/seedu/address/model/ModelManager.java +++ /dev/null @@ -1,153 +0,0 @@ -package seedu.address.model; - -import javafx.collections.transformation.FilteredList; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.UnmodifiableObservableList; -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.core.ComponentManager; -import seedu.address.model.person.Person; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; -import seedu.address.model.person.UniquePersonList.PersonNotFoundException; - -import java.util.Set; -import java.util.logging.Logger; - -/** - * Represents the in-memory model of the address book data. - * All changes to any model should be synchronized. - */ -public class ModelManager extends ComponentManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - - private final AddressBook addressBook; - private final FilteredList filteredPersons; - - /** - * Initializes a ModelManager with the given AddressBook - * AddressBook and its variables should not be null - */ - public ModelManager(AddressBook src, UserPrefs userPrefs) { - super(); - assert src != null; - assert userPrefs != null; - - logger.fine("Initializing with address book: " + src + " and user prefs " + userPrefs); - - addressBook = new AddressBook(src); - filteredPersons = new FilteredList<>(addressBook.getPersons()); - } - - public ModelManager() { - this(new AddressBook(), new UserPrefs()); - } - - public ModelManager(ReadOnlyAddressBook initialData, UserPrefs userPrefs) { - addressBook = new AddressBook(initialData); - filteredPersons = new FilteredList<>(addressBook.getPersons()); - } - - @Override - public void resetData(ReadOnlyAddressBook newData) { - addressBook.resetData(newData); - indicateAddressBookChanged(); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; - } - - /** Raises an event to indicate the model has changed */ - private void indicateAddressBookChanged() { - raise(new AddressBookChangedEvent(addressBook)); - } - - @Override - public synchronized void deletePerson(ReadOnlyPerson target) throws PersonNotFoundException { - addressBook.removePerson(target); - indicateAddressBookChanged(); - } - - @Override - public synchronized void addPerson(Person person) throws UniquePersonList.DuplicatePersonException { - addressBook.addPerson(person); - updateFilteredListToShowAll(); - indicateAddressBookChanged(); - } - - //=========== Filtered Person List Accessors =============================================================== - - @Override - public UnmodifiableObservableList getFilteredPersonList() { - return new UnmodifiableObservableList<>(filteredPersons); - } - - @Override - public void updateFilteredListToShowAll() { - filteredPersons.setPredicate(null); - } - - @Override - public void updateFilteredPersonList(Set keywords){ - updateFilteredPersonList(new PredicateExpression(new NameQualifier(keywords))); - } - - private void updateFilteredPersonList(Expression expression) { - filteredPersons.setPredicate(expression::satisfies); - } - - //========== Inner classes/interfaces used for filtering ================================================== - - interface Expression { - boolean satisfies(ReadOnlyPerson person); - String toString(); - } - - private class PredicateExpression implements Expression { - - private final Qualifier qualifier; - - PredicateExpression(Qualifier qualifier) { - this.qualifier = qualifier; - } - - @Override - public boolean satisfies(ReadOnlyPerson person) { - return qualifier.run(person); - } - - @Override - public String toString() { - return qualifier.toString(); - } - } - - interface Qualifier { - boolean run(ReadOnlyPerson person); - String toString(); - } - - private class NameQualifier implements Qualifier { - private Set nameKeyWords; - - NameQualifier(Set nameKeyWords) { - this.nameKeyWords = nameKeyWords; - } - - @Override - public boolean run(ReadOnlyPerson person) { - return nameKeyWords.stream() - .filter(keyword -> StringUtil.containsIgnoreCase(person.getName().fullName, keyword)) - .findAny() - .isPresent(); - } - - @Override - public String toString() { - return "name=" + String.join(", ", nameKeyWords); - } - } - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index bfca099b1e81..000000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,30 +0,0 @@ -package seedu.address.model; - - -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; - -import java.util.List; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook { - - UniqueTagList getUniqueTagList(); - - UniquePersonList getUniquePersonList(); - - /** - * Returns an unmodifiable view of persons list - */ - List getPersonList(); - - /** - * Returns an unmodifiable view of tags list - */ - List getTagList(); - -} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index a2bd109c005e..000000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,54 +0,0 @@ -package seedu.address.model.person; - - -import seedu.address.commons.exceptions.IllegalValueException; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_ADDRESS_CONSTRAINTS = "Person addresses can be in any format"; - public static final String ADDRESS_VALIDATION_REGEX = ".+"; - - public final String value; - - /** - * Validates given address. - * - * @throws IllegalValueException if given address string is invalid. - */ - public Address(String address) throws IllegalValueException { - assert address != null; - if (!isValidAddress(address)) { - throw new IllegalValueException(MESSAGE_ADDRESS_CONSTRAINTS); - } - this.value = address; - } - - /** - * Returns true if a given string is a valid person email. - */ - public static boolean isValidAddress(String test) { - return test.matches(ADDRESS_VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && this.value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} \ No newline at end of file diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index 5da4d1078236..000000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,56 +0,0 @@ -package seedu.address.model.person; - - -import seedu.address.commons.exceptions.IllegalValueException; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -public class Email { - - public static final String MESSAGE_EMAIL_CONSTRAINTS = - "Person emails should be 2 alphanumeric/period strings separated by '@'"; - public static final String EMAIL_VALIDATION_REGEX = "[\\w\\.]+@[\\w\\.]+"; - - public final String value; - - /** - * Validates given email. - * - * @throws IllegalValueException if given email address string is invalid. - */ - public Email(String email) throws IllegalValueException { - assert email != null; - email = email.trim(); - if (!isValidEmail(email)) { - throw new IllegalValueException(MESSAGE_EMAIL_CONSTRAINTS); - } - this.value = email; - } - - /** - * Returns if a given string is a valid person email. - */ - public static boolean isValidEmail(String test) { - return test.matches(EMAIL_VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && this.value.equals(((Email) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 03ffce7d2e79..000000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,90 +0,0 @@ -package seedu.address.model.person; - -import seedu.address.commons.util.CollectionUtil; -import seedu.address.model.tag.UniqueTagList; - -import java.util.Objects; - -/** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated. - */ -public class Person implements ReadOnlyPerson { - - private Name name; - private Phone phone; - private Email email; - private Address address; - - private UniqueTagList tags; - - /** - * Every field must be present and not null. - */ - public Person(Name name, Phone phone, Email email, Address address, UniqueTagList tags) { - assert !CollectionUtil.isAnyNull(name, phone, email, address, tags); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags = new UniqueTagList(tags); // protect internal tags from changes in the arg list - } - - /** - * Copy constructor. - */ - public Person(ReadOnlyPerson source) { - this(source.getName(), source.getPhone(), source.getEmail(), source.getAddress(), source.getTags()); - } - - @Override - public Name getName() { - return name; - } - - @Override - public Phone getPhone() { - return phone; - } - - @Override - public Email getEmail() { - return email; - } - - @Override - public Address getAddress() { - return address; - } - - @Override - public UniqueTagList getTags() { - return new UniqueTagList(tags); - } - - /** - * Replaces this person's tags with the tags in the argument tag list. - */ - public void setTags(UniqueTagList replacement) { - tags.setTags(replacement); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof ReadOnlyPerson // instanceof handles nulls - && this.isSameStateAs((ReadOnlyPerson) other)); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - return getAsText(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index d27b2244b727..000000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,54 +0,0 @@ -package seedu.address.model.person; - -import seedu.address.commons.exceptions.IllegalValueException; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -public class Phone { - - public static final String MESSAGE_PHONE_CONSTRAINTS = "Person phone numbers should only contain numbers"; - public static final String PHONE_VALIDATION_REGEX = "\\d+"; - - public final String value; - - /** - * Validates given phone number. - * - * @throws IllegalValueException if given phone string is invalid. - */ - public Phone(String phone) throws IllegalValueException { - assert phone != null; - phone = phone.trim(); - if (!isValidPhone(phone)) { - throw new IllegalValueException(MESSAGE_PHONE_CONSTRAINTS); - } - this.value = phone; - } - - /** - * Returns true if a given string is a valid person phone number. - */ - public static boolean isValidPhone(String test) { - return test.matches(PHONE_VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Phone // instanceof handles nulls - && this.value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/ReadOnlyPerson.java b/src/main/java/seedu/address/model/person/ReadOnlyPerson.java deleted file mode 100644 index d45be4b5fe36..000000000000 --- a/src/main/java/seedu/address/model/person/ReadOnlyPerson.java +++ /dev/null @@ -1,65 +0,0 @@ -package seedu.address.model.person; - -import seedu.address.model.tag.UniqueTagList; - -/** - * A read-only immutable interface for a Person in the addressbook. - * Implementations should guarantee: details are present and not null, field values are validated. - */ -public interface ReadOnlyPerson { - - Name getName(); - Phone getPhone(); - Email getEmail(); - Address getAddress(); - - /** - * The returned TagList is a deep copy of the internal TagList, - * changes on the returned list will not affect the person's internal tags. - */ - UniqueTagList getTags(); - - /** - * Returns true if both have the same state. (interfaces cannot override .equals) - */ - default boolean isSameStateAs(ReadOnlyPerson other) { - return other == this // short circuit if same object - || (other != null // this is first to avoid NPE below - && other.getName().equals(this.getName()) // state checks here onwards - && other.getPhone().equals(this.getPhone()) - && other.getEmail().equals(this.getEmail()) - && other.getAddress().equals(this.getAddress())); - } - - /** - * Formats the person as text, showing all contact details. - */ - default String getAsText() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append(" Phone: ") - .append(getPhone()) - .append(" Email: ") - .append(getEmail()) - .append(" Address: ") - .append(getAddress()) - .append(" Tags: "); - getTags().forEach(builder::append); - return builder.toString(); - } - - /** - * Returns a string representation of this Person's tags - */ - default String tagsString() { - final StringBuffer buffer = new StringBuffer(); - final String separator = ", "; - getTags().forEach(tag -> buffer.append(tag).append(separator)); - if (buffer.length() == 0) { - return ""; - } else { - return buffer.substring(0, buffer.length() - separator.length()); - } - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 263f1fcc7dd5..000000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,98 +0,0 @@ -package seedu.address.model.person; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.commons.exceptions.DuplicateDataException; - -import java.util.*; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * - * Supports a minimal set of list operations. - * - * @see Person#equals(Object) - * @see CollectionUtil#elementsAreUnique(Collection) - */ -public class UniquePersonList implements Iterable { - - /** - * Signals that an operation would have violated the 'no duplicates' property of the list. - */ - public static class DuplicatePersonException extends DuplicateDataException { - protected DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } - } - - /** - * Signals that an operation targeting a specified person in the list would fail because - * there is no such matching person in the list. - */ - public static class PersonNotFoundException extends Exception {} - - private final ObservableList internalList = FXCollections.observableArrayList(); - - /** - * Constructs empty PersonList. - */ - public UniquePersonList() {} - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(ReadOnlyPerson toCheck) { - assert toCheck != null; - return internalList.contains(toCheck); - } - - /** - * Adds a person to the list. - * - * @throws DuplicatePersonException if the person to add is a duplicate of an existing person in the list. - */ - public void add(Person toAdd) throws DuplicatePersonException { - assert toAdd != null; - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Removes the equivalent person from the list. - * - * @throws PersonNotFoundException if no such person could be found in the list. - */ - public boolean remove(ReadOnlyPerson toRemove) throws PersonNotFoundException { - assert toRemove != null; - final boolean personFoundAndDeleted = internalList.remove(toRemove); - if (!personFoundAndDeleted) { - throw new PersonNotFoundException(); - } - return personFoundAndDeleted; - } - - public ObservableList getInternalList() { - return internalList; - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && this.internalList.equals( - ((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index 5bcffdb5ddf1..000000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.tag; - - -import seedu.address.commons.exceptions.IllegalValueException; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_TAG_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String TAG_VALIDATION_REGEX = "\\p{Alnum}+"; - - public String tagName; - - public Tag() { - } - - /** - * Validates given tag name. - * - * @throws IllegalValueException if the given tag name string is invalid. - */ - public Tag(String name) throws IllegalValueException { - assert name != null; - name = name.trim(); - if (!isValidTagName(name)) { - throw new IllegalValueException(MESSAGE_TAG_CONSTRAINTS); - } - this.tagName = name; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(TAG_VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Tag // instanceof handles nulls - && this.tagName.equals(((Tag) other).tagName)); // state check - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/tag/UniqueTagList.java b/src/main/java/seedu/address/model/tag/UniqueTagList.java deleted file mode 100644 index 76fb7ff3dc5d..000000000000 --- a/src/main/java/seedu/address/model/tag/UniqueTagList.java +++ /dev/null @@ -1,143 +0,0 @@ -package seedu.address.model.tag; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.commons.exceptions.DuplicateDataException; - -import java.util.*; - -/** - * A list of tags that enforces no nulls and uniqueness between its elements. - * - * Supports minimal set of list operations for the app's features. - * - * @see Tag#equals(Object) - * @see CollectionUtil#elementsAreUnique(Collection) - */ -public class UniqueTagList implements Iterable { - - /** - * Signals that an operation would have violated the 'no duplicates' property of the list. - */ - public static class DuplicateTagException extends DuplicateDataException { - protected DuplicateTagException() { - super("Operation would result in duplicate tags"); - } - } - - private final ObservableList internalList = FXCollections.observableArrayList(); - - /** - * Constructs empty TagList. - */ - public UniqueTagList() {} - - /** - * Varargs/array constructor, enforces no nulls or duplicates. - */ - public UniqueTagList(Tag... tags) throws DuplicateTagException { - assert !CollectionUtil.isAnyNull((Object[]) tags); - final List initialTags = Arrays.asList(tags); - if (!CollectionUtil.elementsAreUnique(initialTags)) { - throw new DuplicateTagException(); - } - internalList.addAll(initialTags); - } - - /** - * java collections constructor, enforces no null or duplicate elements. - */ - public UniqueTagList(Collection tags) throws DuplicateTagException { - CollectionUtil.assertNoNullElements(tags); - if (!CollectionUtil.elementsAreUnique(tags)) { - throw new DuplicateTagException(); - } - internalList.addAll(tags); - } - - /** - * java set constructor, enforces no nulls. - */ - public UniqueTagList(Set tags) { - CollectionUtil.assertNoNullElements(tags); - internalList.addAll(tags); - } - - /** - * Copy constructor, insulates from changes in source. - */ - public UniqueTagList(UniqueTagList source) { - internalList.addAll(source.internalList); // insulate internal list from changes in argument - } - - /** - * All tags in this list as a Set. This set is mutable and change-insulated against the internal list. - */ - public Set toSet() { - return new HashSet<>(internalList); - } - - /** - * Replaces the Tags in this list with those in the argument tag list. - */ - public void setTags(UniqueTagList replacement) { - this.internalList.clear(); - this.internalList.addAll(replacement.internalList); - } - - /** - * Adds every tag from the argument list that does not yet exist in this list. - */ - public void mergeFrom(UniqueTagList tags) { - final Set alreadyInside = this.toSet(); - for (Tag tag : tags) { - if (!alreadyInside.contains(tag)) { - internalList.add(tag); - } - } - } - - /** - * Returns true if the list contains an equivalent Tag as the given argument. - */ - public boolean contains(Tag toCheck) { - assert toCheck != null; - return internalList.contains(toCheck); - } - - /** - * Adds a Tag to the list. - * - * @throws DuplicateTagException if the Tag to add is a duplicate of an existing Tag in the list. - */ - public void add(Tag toAdd) throws DuplicateTagException { - assert toAdd != null; - if (contains(toAdd)) { - throw new DuplicateTagException(); - } - internalList.add(toAdd); - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - public ObservableList getInternalList() { - return internalList; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniqueTagList // instanceof handles nulls - && this.internalList.equals( - ((UniqueTagList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } -} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 80033086985b..000000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,44 +0,0 @@ -package seedu.address.storage; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -import java.io.IOException; -import java.util.Optional; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - String getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(String filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java deleted file mode 100644 index 91002a8a821a..000000000000 --- a/src/main/java/seedu/address/storage/Storage.java +++ /dev/null @@ -1,39 +0,0 @@ -package seedu.address.storage; - -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.events.storage.DataSavingExceptionEvent; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.UserPrefs; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Optional; - -/** - * API of the Storage component - */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { - - @Override - Optional readUserPrefs() throws DataConversionException, IOException; - - @Override - void saveUserPrefs(UserPrefs userPrefs) throws IOException; - - @Override - String getAddressBookFilePath(); - - @Override - Optional readAddressBook() throws DataConversionException, IOException; - - @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * Saves the current version of the Address Book to the hard disk. - * Creates the data file if it is missing. - * Raises {@link DataSavingExceptionEvent} if there was an error during saving. - */ - void handleAddressBookChangedEvent(AddressBookChangedEvent abce); -} diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java deleted file mode 100644 index ba1f72f15c27..000000000000 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ /dev/null @@ -1,91 +0,0 @@ -package seedu.address.storage; - -import com.google.common.eventbus.Subscribe; -import seedu.address.commons.core.ComponentManager; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.events.storage.DataSavingExceptionEvent; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.UserPrefs; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * Manages storage of AddressBook data in local storage. - */ -public class StorageManager extends ComponentManager implements Storage { - - private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; - private UserPrefsStorage userPrefsStorage; - - - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { - super(); - this.addressBookStorage = addressBookStorage; - this.userPrefsStorage = userPrefsStorage; - } - - public StorageManager(String addressBookFilePath, String userPrefsFilePath) { - this(new XmlAddressBookStorage(addressBookFilePath), new JsonUserPrefsStorage(userPrefsFilePath)); - } - - // ================ UserPrefs methods ============================== - - @Override - public Optional readUserPrefs() throws DataConversionException, IOException { - return userPrefsStorage.readUserPrefs(); - } - - @Override - public void saveUserPrefs(UserPrefs userPrefs) throws IOException { - userPrefsStorage.saveUserPrefs(userPrefs); - } - - - // ================ AddressBook methods ============================== - - @Override - public String getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); - } - - @Override - public Optional readAddressBook(String filePath) throws DataConversionException, IOException { - logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) throws IOException { - logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); - } - - - @Override - @Subscribe - public void handleAddressBookChangedEvent(AddressBookChangedEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local data changed, saving to file")); - try { - saveAddressBook(event.data); - } catch (IOException e) { - raise(new DataSavingExceptionEvent(e)); - } - } - -} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java deleted file mode 100644 index f2167ec201b4..000000000000 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ /dev/null @@ -1,68 +0,0 @@ -package seedu.address.storage; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.*; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; - -import javax.xml.bind.annotation.XmlElement; -import java.util.ArrayList; -import java.util.List; - -/** - * JAXB-friendly version of the Person. - */ -public class XmlAdaptedPerson { - - @XmlElement(required = true) - private String name; - @XmlElement(required = true) - private String phone; - @XmlElement(required = true) - private String email; - @XmlElement(required = true) - private String address; - - @XmlElement - private List tagged = new ArrayList<>(); - - /** - * No-arg constructor for JAXB use. - */ - public XmlAdaptedPerson() {} - - - /** - * Converts a given Person into this class for JAXB use. - * - * @param source future changes to this will not affect the created XmlAdaptedPerson - */ - public XmlAdaptedPerson(ReadOnlyPerson source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged = new ArrayList<>(); - for (Tag tag : source.getTags()) { - tagged.add(new XmlAdaptedTag(tag)); - } - } - - /** - * Converts this jaxb-friendly adapted person object into the model's Person object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person - */ - public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (XmlAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); - } - final Name name = new Name(this.name); - final Phone phone = new Phone(this.phone); - final Email email = new Email(this.email); - final Address address = new Address(this.address); - final UniqueTagList tags = new UniqueTagList(personTags); - return new Person(name, phone, email, address, tags); - } -} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedTag.java b/src/main/java/seedu/address/storage/XmlAdaptedTag.java deleted file mode 100644 index b9723fafbc67..000000000000 --- a/src/main/java/seedu/address/storage/XmlAdaptedTag.java +++ /dev/null @@ -1,40 +0,0 @@ -package seedu.address.storage; - -import seedu.address.commons.util.CollectionUtil; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -import javax.xml.bind.annotation.XmlValue; - -/** - * JAXB-friendly adapted version of the Tag. - */ -public class XmlAdaptedTag { - - @XmlValue - public String tagName; - - /** - * No-arg constructor for JAXB use. - */ - public XmlAdaptedTag() {} - - /** - * Converts a given Tag into this class for JAXB use. - * - * @param source future changes to this will not affect the created - */ - public XmlAdaptedTag(Tag source) { - tagName = source.tagName; - } - - /** - * Converts this jaxb-friendly adapted tag object into the model's Tag object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person - */ - public Tag toModelType() throws IllegalValueException { - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java deleted file mode 100644 index 30cb00270cc4..000000000000 --- a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java +++ /dev/null @@ -1,73 +0,0 @@ -package seedu.address.storage; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.FileUtil; -import seedu.address.model.ReadOnlyAddressBook; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * A class to access AddressBook data stored as an xml file on the hard disk. - */ -public class XmlAddressBookStorage implements AddressBookStorage { - - private static final Logger logger = LogsCenter.getLogger(XmlAddressBookStorage.class); - - private String filePath; - - public XmlAddressBookStorage(String filePath){ - this.filePath = filePath; - } - - public String getAddressBookFilePath(){ - return filePath; - } - - /** - * Similar to {@link #readAddressBook()} - * @param filePath location of the data. Cannot be null - * @throws DataConversionException if the file is not in the correct format. - */ - public Optional readAddressBook(String filePath) throws DataConversionException, FileNotFoundException { - assert filePath != null; - - File addressBookFile = new File(filePath); - - if (!addressBookFile.exists()) { - logger.info("AddressBook file " + addressBookFile + " not found"); - return Optional.empty(); - } - - ReadOnlyAddressBook addressBookOptional = XmlFileStorage.loadDataFromSaveFile(new File(filePath)); - - return Optional.of(addressBookOptional); - } - - /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)} - * @param filePath location of the data. Cannot be null - */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) throws IOException { - assert addressBook != null; - assert filePath != null; - - File file = new File(filePath); - FileUtil.createIfMissing(file); - XmlFileStorage.saveDataToFile(file, new XmlSerializableAddressBook(addressBook)); - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } -} diff --git a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java deleted file mode 100644 index b7ec533a3a1e..000000000000 --- a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java +++ /dev/null @@ -1,88 +0,0 @@ -package seedu.address.storage; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -/** - * An Immutable AddressBook that is serializable to XML format - */ -@XmlRootElement(name = "addressbook") -public class XmlSerializableAddressBook implements ReadOnlyAddressBook { - - @XmlElement - private List persons; - @XmlElement - private List tags; - - { - persons = new ArrayList<>(); - tags = new ArrayList<>(); - } - - /** - * Empty constructor required for marshalling - */ - public XmlSerializableAddressBook() {} - - /** - * Conversion - */ - public XmlSerializableAddressBook(ReadOnlyAddressBook src) { - persons.addAll(src.getPersonList().stream().map(XmlAdaptedPerson::new).collect(Collectors.toList())); - tags = src.getTagList(); - } - - @Override - public UniqueTagList getUniqueTagList() { - try { - return new UniqueTagList(tags); - } catch (UniqueTagList.DuplicateTagException e) { - //TODO: better error handling - e.printStackTrace(); - return null; - } - } - - @Override - public UniquePersonList getUniquePersonList() { - UniquePersonList lists = new UniquePersonList(); - for (XmlAdaptedPerson p : persons) { - try { - lists.add(p.toModelType()); - } catch (IllegalValueException e) { - //TODO: better error handling - } - } - return lists; - } - - @Override - public List getPersonList() { - return persons.stream().map(p -> { - try { - return p.toModelType(); - } catch (IllegalValueException e) { - e.printStackTrace(); - //TODO: better error handling - return null; - } - }).collect(Collectors.toCollection(ArrayList::new)); - } - - @Override - public List getTagList() { - return Collections.unmodifiableList(tags); - } - -} diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java deleted file mode 100644 index 54b88318019b..000000000000 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ /dev/null @@ -1,68 +0,0 @@ -package seedu.address.ui; - -import javafx.event.Event; -import javafx.scene.Node; -import javafx.scene.layout.AnchorPane; -import javafx.scene.web.WebView; -import seedu.address.commons.util.FxViewUtil; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.commons.core.LogsCenter; - -import java.util.logging.Logger; - -/** - * The Browser Panel of the App. - */ -public class BrowserPanel extends UiPart{ - - private static Logger logger = LogsCenter.getLogger(BrowserPanel.class); - private WebView browser; - - /** - * Constructor is kept private as {@link #load(AnchorPane)} is the only way to create a BrowserPanel. - */ - private BrowserPanel() { - - } - - @Override - public void setNode(Node node) { - //not applicable - } - - @Override - public String getFxmlPath() { - return null; //not applicable - } - - /** - * Factory method for creating a Browser Panel. - * This method should be called after the FX runtime is initialized and in FX application thread. - * @param placeholder The AnchorPane where the BrowserPanel must be inserted - */ - public static BrowserPanel load(AnchorPane placeholder){ - logger.info("Initializing browser"); - BrowserPanel browserPanel = new BrowserPanel(); - browserPanel.browser = new WebView(); - placeholder.setOnKeyPressed(Event::consume); // To prevent triggering events for typing inside the loaded Web page. - FxViewUtil.applyAnchorBoundaryParameters(browserPanel.browser, 0.0, 0.0, 0.0, 0.0); - placeholder.getChildren().add(browserPanel.browser); - return browserPanel; - } - - public void loadPersonPage(ReadOnlyPerson person) { - loadPage("https://www.google.com.sg/#safe=off&q=" + person.getName().fullName.replaceAll(" ", "+")); - } - - public void loadPage(String url){ - browser.getEngine().load(url); - } - - /** - * Frees resources allocated to the browser. - */ - public void freeResources() { - browser = null; - } - -} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java deleted file mode 100644 index 2e1409a3016c..000000000000 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ /dev/null @@ -1,114 +0,0 @@ -package seedu.address.ui; - -import com.google.common.eventbus.Subscribe; -import javafx.fxml.FXML; -import javafx.scene.Node; -import javafx.scene.control.SplitPane; -import javafx.scene.control.TextField; -import javafx.scene.layout.AnchorPane; -import javafx.stage.Stage; -import seedu.address.commons.events.ui.IncorrectCommandAttemptedEvent; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.*; -import seedu.address.commons.util.FxViewUtil; -import seedu.address.commons.core.LogsCenter; - -import java.util.logging.Logger; - -public class CommandBox extends UiPart { - private final Logger logger = LogsCenter.getLogger(CommandBox.class); - private static final String FXML = "CommandBox.fxml"; - - private AnchorPane placeHolderPane; - private AnchorPane commandPane; - private ResultDisplay resultDisplay; - String previousCommandTest; - - private Logic logic; - - @FXML - private TextField commandTextField; - private CommandResult mostRecentResult; - - public static CommandBox load(Stage primaryStage, AnchorPane commandBoxPlaceholder, - ResultDisplay resultDisplay, Logic logic) { - CommandBox commandBox = UiPartLoader.loadUiPart(primaryStage, commandBoxPlaceholder, new CommandBox()); - commandBox.configure(resultDisplay, logic); - commandBox.addToPlaceholder(); - return commandBox; - } - - public void configure(ResultDisplay resultDisplay, Logic logic) { - this.resultDisplay = resultDisplay; - this.logic = logic; - registerAsAnEventHandler(this); - } - - private void addToPlaceholder() { - SplitPane.setResizableWithParent(placeHolderPane, false); - placeHolderPane.getChildren().add(commandTextField); - FxViewUtil.applyAnchorBoundaryParameters(commandPane, 0.0, 0.0, 0.0, 0.0); - FxViewUtil.applyAnchorBoundaryParameters(commandTextField, 0.0, 0.0, 0.0, 0.0); - } - - @Override - public void setNode(Node node) { - commandPane = (AnchorPane) node; - } - - @Override - public String getFxmlPath() { - return FXML; - } - - @Override - public void setPlaceholder(AnchorPane pane) { - this.placeHolderPane = pane; - } - - - @FXML - private void handleCommandInputChanged() { - //Take a copy of the command text - previousCommandTest = commandTextField.getText(); - - /* We assume the command is correct. If it is incorrect, the command box will be changed accordingly - * in the event handling code {@link #handleIncorrectCommandAttempted} - */ - setStyleToIndicateCorrectCommand(); - mostRecentResult = logic.execute(previousCommandTest); - resultDisplay.postMessage(mostRecentResult.feedbackToUser); - logger.info("Result: " + mostRecentResult.feedbackToUser); - } - - - /** - * Sets the command box style to indicate a correct command. - */ - private void setStyleToIndicateCorrectCommand() { - commandTextField.getStyleClass().remove("error"); - commandTextField.setText(""); - } - - @Subscribe - private void handleIncorrectCommandAttempted(IncorrectCommandAttemptedEvent event){ - logger.info(LogsCenter.getEventHandlingLogMessage(event,"Invalid command: " + previousCommandTest)); - setStyleToIndicateIncorrectCommand(); - restoreCommandText(); - } - - /** - * Restores the command box text to the previously entered command - */ - private void restoreCommandText() { - commandTextField.setText(previousCommandTest); - } - - /** - * Sets the command box style to indicate an error - */ - private void setStyleToIndicateIncorrectCommand() { - commandTextField.getStyleClass().add("error"); - } - -} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java deleted file mode 100644 index 45b765ab6a0c..000000000000 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ /dev/null @@ -1,62 +0,0 @@ -package seedu.address.ui; - -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.layout.AnchorPane; -import javafx.scene.web.WebView; -import javafx.stage.Stage; -import seedu.address.commons.util.FxViewUtil; -import seedu.address.commons.core.LogsCenter; - -import java.util.logging.Logger; - -/** - * Controller for a help page - */ -public class HelpWindow extends UiPart { - - private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); - private static final String ICON = "/images/help_icon.png"; - private static final String FXML = "HelpWindow.fxml"; - private static final String TITLE = "Help"; - private static final String USERGUIDE_URL = - "https://github.com/se-edu/addressbook-level4/blob/master/docs/UserGuide.md"; - - private AnchorPane mainPane; - - private Stage dialogStage; - - public static HelpWindow load(Stage primaryStage) { - logger.fine("Showing help page about the application."); - HelpWindow helpWindow = UiPartLoader.loadUiPart(primaryStage, new HelpWindow()); - helpWindow.configure(); - return helpWindow; - } - - @Override - public void setNode(Node node) { - mainPane = (AnchorPane) node; - } - - @Override - public String getFxmlPath() { - return FXML; - } - - private void configure(){ - Scene scene = new Scene(mainPane); - //Null passed as the parent stage to make it non-modal. - dialogStage = createDialogStage(TITLE, null, scene); - dialogStage.setMaximized(true); //TODO: set a more appropriate initial size - setIcon(dialogStage, ICON); - - WebView browser = new WebView(); - browser.getEngine().load(USERGUIDE_URL); - FxViewUtil.applyAnchorBoundaryParameters(browser, 0.0, 0.0, 0.0, 0.0); - mainPane.getChildren().add(browser); - } - - public void show() { - dialogStage.showAndWait(); - } -} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java deleted file mode 100644 index 2c76aced3b04..000000000000 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ /dev/null @@ -1,196 +0,0 @@ -package seedu.address.ui; - -import javafx.fxml.FXML; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.control.MenuItem; -import javafx.scene.input.KeyCombination; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.VBox; -import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.events.ui.ExitAppRequestEvent; -import seedu.address.logic.Logic; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.ReadOnlyPerson; - -/** - * The Main Window. Provides the basic application layout containing - * a menu bar and space where other JavaFX elements can be placed. - */ -public class MainWindow extends UiPart { - - private static final String ICON = "/images/address_book_32.png"; - private static final String FXML = "MainWindow.fxml"; - public static final int MIN_HEIGHT = 600; - public static final int MIN_WIDTH = 450; - - private Logic logic; - - // Independent Ui parts residing in this Ui container - private BrowserPanel browserPanel; - private PersonListPanel personListPanel; - private ResultDisplay resultDisplay; - private StatusBarFooter statusBarFooter; - private CommandBox commandBox; - private Config config; - private UserPrefs userPrefs; - - // Handles to elements of this Ui container - private VBox rootLayout; - private Scene scene; - - private String addressBookName; - - @FXML - private AnchorPane browserPlaceholder; - - @FXML - private AnchorPane commandBoxPlaceholder; - - @FXML - private MenuItem helpMenuItem; - - @FXML - private AnchorPane personListPanelPlaceholder; - - @FXML - private AnchorPane resultDisplayPlaceholder; - - @FXML - private AnchorPane statusbarPlaceholder; - - - public MainWindow() { - super(); - } - - @Override - public void setNode(Node node) { - rootLayout = (VBox) node; - } - - @Override - public String getFxmlPath() { - return FXML; - } - - public static MainWindow load(Stage primaryStage, Config config, UserPrefs prefs, Logic logic) { - - MainWindow mainWindow = UiPartLoader.loadUiPart(primaryStage, new MainWindow()); - mainWindow.configure(config.getAppTitle(), config.getAddressBookName(), config, prefs, logic); - return mainWindow; - } - - private void configure(String appTitle, String addressBookName, Config config, UserPrefs prefs, - Logic logic) { - - //Set dependencies - this.logic = logic; - this.addressBookName = addressBookName; - this.config = config; - this.userPrefs = prefs; - - //Configure the UI - setTitle(appTitle); - setIcon(ICON); - setWindowMinSize(); - setWindowDefaultSize(prefs); - scene = new Scene(rootLayout); - primaryStage.setScene(scene); - - setAccelerators(); - } - - private void setAccelerators() { - helpMenuItem.setAccelerator(KeyCombination.valueOf("F1")); - } - - void fillInnerParts() { - browserPanel = BrowserPanel.load(browserPlaceholder); - personListPanel = PersonListPanel.load(primaryStage, getPersonListPlaceholder(), logic.getFilteredPersonList()); - resultDisplay = ResultDisplay.load(primaryStage, getResultDisplayPlaceholder()); - statusBarFooter = StatusBarFooter.load(primaryStage, getStatusbarPlaceholder(), config.getAddressBookFilePath()); - commandBox = CommandBox.load(primaryStage, getCommandBoxPlaceholder(), resultDisplay, logic); - } - - private AnchorPane getCommandBoxPlaceholder() { - return commandBoxPlaceholder; - } - - private AnchorPane getStatusbarPlaceholder() { - return statusbarPlaceholder; - } - - private AnchorPane getResultDisplayPlaceholder() { - return resultDisplayPlaceholder; - } - - public AnchorPane getPersonListPlaceholder() { - return personListPanelPlaceholder; - } - - public void hide() { - primaryStage.hide(); - } - - private void setTitle(String appTitle) { - primaryStage.setTitle(appTitle); - } - - /** - * Sets the default size based on user preferences. - */ - protected void setWindowDefaultSize(UserPrefs prefs) { - primaryStage.setHeight(prefs.getGuiSettings().getWindowHeight()); - primaryStage.setWidth(prefs.getGuiSettings().getWindowWidth()); - if (prefs.getGuiSettings().getWindowCoordinates() != null) { - primaryStage.setX(prefs.getGuiSettings().getWindowCoordinates().getX()); - primaryStage.setY(prefs.getGuiSettings().getWindowCoordinates().getY()); - } - } - - private void setWindowMinSize() { - primaryStage.setMinHeight(MIN_HEIGHT); - primaryStage.setMinWidth(MIN_WIDTH); - } - - /** - * Returns the current size and the position of the main Window. - */ - public GuiSettings getCurrentGuiSetting() { - return new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); - } - - @FXML - public void handleHelp() { - HelpWindow helpWindow = HelpWindow.load(primaryStage); - helpWindow.show(); - } - - public void show() { - primaryStage.show(); - } - - /** - * Closes the application. - */ - @FXML - private void handleExit() { - raise(new ExitAppRequestEvent()); - } - - public PersonListPanel getPersonListPanel() { - return this.personListPanel; - } - - public void loadPersonPage(ReadOnlyPerson person) { - browserPanel.loadPersonPage(person); - } - - public void releaseResources() { - browserPanel.freeResources(); - } -} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index 259e9ad0d333..000000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,65 +0,0 @@ -package seedu.address.ui; - -import javafx.fxml.FXML; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import seedu.address.model.person.ReadOnlyPerson; - -public class PersonCard extends UiPart{ - - private static final String FXML = "PersonListCard.fxml"; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private Label tags; - - private ReadOnlyPerson person; - private int displayedIndex; - - public PersonCard(){ - - } - - public static PersonCard load(ReadOnlyPerson person, int displayedIndex){ - PersonCard card = new PersonCard(); - card.person = person; - card.displayedIndex = displayedIndex; - return UiPartLoader.loadUiPart(card); - } - - @FXML - public void initialize() { - name.setText(person.getName().fullName); - id.setText(displayedIndex + ". "); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - tags.setText(person.tagsString()); - } - - public HBox getLayout() { - return cardPane; - } - - @Override - public void setNode(Node node) { - cardPane = (HBox)node; - } - - @Override - public String getFxmlPath() { - return FXML; - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index 27d9381c47b5..000000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,108 +0,0 @@ -package seedu.address.ui; - -import javafx.application.Platform; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.Node; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.control.SplitPane; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.VBox; -import javafx.stage.Stage; -import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.commons.core.LogsCenter; - -import java.util.logging.Logger; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - private static final String FXML = "PersonListPanel.fxml"; - private VBox panel; - private AnchorPane placeHolderPane; - - @FXML - private ListView personListView; - - public PersonListPanel() { - super(); - } - - @Override - public void setNode(Node node) { - panel = (VBox) node; - } - - @Override - public String getFxmlPath() { - return FXML; - } - - @Override - public void setPlaceholder(AnchorPane pane) { - this.placeHolderPane = pane; - } - - public static PersonListPanel load(Stage primaryStage, AnchorPane personListPlaceholder, - ObservableList personList) { - PersonListPanel personListPanel = - UiPartLoader.loadUiPart(primaryStage, personListPlaceholder, new PersonListPanel()); - personListPanel.configure(personList); - return personListPanel; - } - - private void configure(ObservableList personList) { - setConnections(personList); - addToPlaceholder(); - } - - private void setConnections(ObservableList personList) { - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - setEventHandlerForSelectionChangeEvent(); - } - - private void addToPlaceholder() { - SplitPane.setResizableWithParent(placeHolderPane, false); - placeHolderPane.getChildren().add(panel); - } - - private void setEventHandlerForSelectionChangeEvent() { - personListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - logger.fine("Selection in person list panel changed to : '" + newValue + "'"); - raise(new PersonPanelSelectionChangedEvent(newValue)); - } - }); - } - - public void scrollTo(int index) { - Platform.runLater(() -> { - personListView.scrollTo(index); - personListView.getSelectionModel().clearAndSelect(index); - }); - } - - class PersonListViewCell extends ListCell { - - public PersonListViewCell() { - } - - @Override - protected void updateItem(ReadOnlyPerson person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(PersonCard.load(person, getIndex() + 1).getLayout()); - } - } - } - -} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java deleted file mode 100644 index 37284ee6c696..000000000000 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ /dev/null @@ -1,65 +0,0 @@ -package seedu.address.ui; - -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.scene.Node; -import javafx.scene.control.TextArea; -import javafx.scene.layout.AnchorPane; -import javafx.stage.Stage; -import seedu.address.commons.util.FxViewUtil; - -/** - * A ui for the status bar that is displayed at the header of the application. - */ -public class ResultDisplay extends UiPart { - public static final String RESULT_DISPLAY_ID = "resultDisplay"; - private static final String STATUS_BAR_STYLE_SHEET = "result-display"; - private TextArea resultDisplayArea; - private final StringProperty displayed = new SimpleStringProperty(""); - - private static final String FXML = "ResultDisplay.fxml"; - - private AnchorPane placeHolder; - - private AnchorPane mainPane; - - public static ResultDisplay load(Stage primaryStage, AnchorPane placeHolder) { - ResultDisplay statusBar = UiPartLoader.loadUiPart(primaryStage, placeHolder, new ResultDisplay()); - statusBar.configure(); - return statusBar; - } - - public void configure() { - resultDisplayArea = new TextArea(); - resultDisplayArea.setEditable(false); - resultDisplayArea.setId(RESULT_DISPLAY_ID); - resultDisplayArea.getStyleClass().removeAll(); - resultDisplayArea.getStyleClass().add(STATUS_BAR_STYLE_SHEET); - resultDisplayArea.setText(""); - resultDisplayArea.textProperty().bind(displayed); - FxViewUtil.applyAnchorBoundaryParameters(resultDisplayArea, 0.0, 0.0, 0.0, 0.0); - mainPane.getChildren().add(resultDisplayArea); - FxViewUtil.applyAnchorBoundaryParameters(mainPane, 0.0, 0.0, 0.0, 0.0); - placeHolder.getChildren().add(mainPane); - } - - @Override - public void setNode(Node node) { - mainPane = (AnchorPane) node; - } - - @Override - public void setPlaceholder(AnchorPane placeholder) { - this.placeHolder = placeholder; - } - - @Override - public String getFxmlPath() { - return FXML; - } - - public void postMessage(String message) { - displayed.setValue(message); - } - -} diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/agendum/MainApp.java similarity index 60% rename from src/main/java/seedu/address/MainApp.java rename to src/main/java/seedu/agendum/MainApp.java index 36dc72a74b7a..c240b1a011d1 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/agendum/MainApp.java @@ -1,28 +1,29 @@ -package seedu.address; +package seedu.agendum; import com.google.common.eventbus.Subscribe; import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.events.ui.ExitAppRequestEvent; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.*; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; - -import java.io.FileNotFoundException; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.core.Version; +import seedu.agendum.commons.events.ui.ExitAppRequestEvent; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.logic.Logic; +import seedu.agendum.logic.LogicManager; +import seedu.agendum.logic.commands.CommandLibrary; +import seedu.agendum.model.*; +import seedu.agendum.commons.util.ConfigUtil; +import seedu.agendum.storage.Storage; +import seedu.agendum.storage.StorageManager; +import seedu.agendum.ui.Ui; +import seedu.agendum.ui.UiManager; + import java.io.IOException; import java.util.Map; +import java.util.Hashtable; import java.util.Optional; import java.util.logging.Logger; @@ -45,19 +46,22 @@ public MainApp() {} @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing Agendum ]==========================="); super.init(); config = initConfig(getApplicationParameter("config")); - storage = new StorageManager(config.getAddressBookFilePath(), config.getUserPrefsFilePath()); + storage = new StorageManager(config.getToDoListFilePath(), config.getAliasTableFilePath(), + config.getUserPrefsFilePath(), config); userPrefs = initPrefs(config); + + initAliasTable(config); initLogging(config); model = initModelManager(storage, userPrefs); - logic = new LogicManager(model, storage); + logic = new LogicManager(model); ui = new UiManager(logic, config, userPrefs); @@ -70,23 +74,23 @@ private String getApplicationParameter(String parameterName){ } private Model initModelManager(Storage storage, UserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + Optional toDoListOptional; + ReadOnlyToDoList initialData; try { - addressBookOptional = storage.readAddressBook(); - if(!addressBookOptional.isPresent()){ - logger.info("Data file not found. Will be starting with an empty AddressBook"); + toDoListOptional = storage.readToDoList(); + if(!toDoListOptional.isPresent()){ + logger.info("Data file not found. Will be starting with an empty ToDoList"); } - initialData = addressBookOptional.orElse(new AddressBook()); + initialData = toDoListOptional.orElse(new ToDoList()); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Data file not in the correct format. Will be starting with an empty ToDoList"); + initialData = new ToDoList(); } catch (IOException e) { - logger.warning("Problem while reading from the file. . Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Problem while reading from the file. Will be starting with an empty ToDoList"); + initialData = new ToDoList(); } - return new ModelManager(initialData, userPrefs); + return new ModelManager(initialData); } private void initLogging(Config config) { @@ -117,6 +121,7 @@ protected Config initConfig(String configFilePath) { //Update config file in case it was missing to begin with or there are new/unused fields try { + initializedConfig.setConfigFilePath(configFilePathUsed); ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); } catch (IOException e) { logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); @@ -139,7 +144,7 @@ protected UserPrefs initPrefs(Config config) { "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. . Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. . Will be starting with an empty ToDoList"); initializedPrefs = new UserPrefs(); } @@ -153,19 +158,48 @@ protected UserPrefs initPrefs(Config config) { return initializedPrefs; } + protected void initAliasTable(Config config) { + assert config != null; + + String aliasTableFilePath = config.getAliasTableFilePath(); + logger.info("Using alias table file : " + aliasTableFilePath); + + Hashtable initializedAliasTable; + try { + Optional> aliasTableOptional = storage.readAliasTable(); + initializedAliasTable = aliasTableOptional.orElse(new Hashtable()); + } catch (DataConversionException e) { + logger.warning("Alias table file at " + aliasTableFilePath + + " is not in the correct format. Using default empty alias table"); + initializedAliasTable = new Hashtable(); + } catch (IOException e) { + logger.warning("Problem while reading from the alias table file. Will start with an empty alias table"); + initializedAliasTable = new Hashtable(); + } + + CommandLibrary.getInstance().loadAliasTable(initializedAliasTable); + + //Update alias table file in case it was missing initially or there are new/unused fields + try { + storage.saveAliasTable(initializedAliasTable); + } catch (IOException e) { + logger.warning("Failed to save alias table file : " + StringUtil.getDetails(e)); + } + } + private void initEventsCenter() { EventsCenter.getInstance().registerHandler(this); } @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting ToDoList " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping Agendum ] ============================="); ui.stop(); try { storage.saveUserPrefs(userPrefs); diff --git a/src/main/java/seedu/address/commons/core/ComponentManager.java b/src/main/java/seedu/agendum/commons/core/ComponentManager.java similarity index 87% rename from src/main/java/seedu/address/commons/core/ComponentManager.java rename to src/main/java/seedu/agendum/commons/core/ComponentManager.java index 4bc8564e5824..4b289c3b1577 100644 --- a/src/main/java/seedu/address/commons/core/ComponentManager.java +++ b/src/main/java/seedu/agendum/commons/core/ComponentManager.java @@ -1,6 +1,6 @@ -package seedu.address.commons.core; +package seedu.agendum.commons.core; -import seedu.address.commons.events.BaseEvent; +import seedu.agendum.commons.events.BaseEvent; /** * Base class for *Manager classes diff --git a/src/main/java/seedu/agendum/commons/core/Config.java b/src/main/java/seedu/agendum/commons/core/Config.java new file mode 100644 index 000000000000..22a5b31db408 --- /dev/null +++ b/src/main/java/seedu/agendum/commons/core/Config.java @@ -0,0 +1,121 @@ +package seedu.agendum.commons.core; + +import java.util.Objects; +import java.util.logging.Level; + +/** + * Config values used by the app + */ +public class Config { + + public static final String DEFAULT_DATA_DIR = "data/"; + public static final String DEFAULT_JSON_DIR = "json/"; + public static final String DEFAULT_CONFIG_FILE = DEFAULT_DATA_DIR + DEFAULT_JSON_DIR + "config.json"; + public static final String DEFAULT_ALIAS_TABLE_FILE = DEFAULT_DATA_DIR + DEFAULT_JSON_DIR + "commands.json"; + public static final String DEFAULT_USER_PREFS_FILE = DEFAULT_DATA_DIR + DEFAULT_JSON_DIR + "preferences.json"; + public static final String DEFAULT_SAVE_LOCATION = DEFAULT_DATA_DIR + "todolist.xml"; + + // Config values customizable through config file + private String appTitle = "Agendum"; + private Level logLevel = Level.INFO; + private String aliasTableFilePath = DEFAULT_ALIAS_TABLE_FILE; + private String userPrefsFilePath = DEFAULT_USER_PREFS_FILE; + private String toDoListFilePath = DEFAULT_SAVE_LOCATION; + private String configFilePath = DEFAULT_CONFIG_FILE; + private String toDoListName = "MyToDoList"; + + public String getAppTitle() { + return appTitle; + } + + public void setAppTitle(String appTitle) { + this.appTitle = appTitle; + } + + public Level getLogLevel() { + return logLevel; + } + + public void setLogLevel(Level logLevel) { + this.logLevel = logLevel; + } + + public String getAliasTableFilePath() { + return aliasTableFilePath; + } + + public void setAliasTableFilePath(String aliasTableFilePath) { + this.aliasTableFilePath = aliasTableFilePath; + } + + public String getUserPrefsFilePath() { + return userPrefsFilePath; + } + + public void setUserPrefsFilePath(String userPrefsFilePath) { + this.userPrefsFilePath = userPrefsFilePath; + } + + public String getToDoListFilePath() { + return toDoListFilePath; + } + + public void setToDoListFilePath(String toDoListFilePath) { + this.toDoListFilePath = toDoListFilePath; + } + + public String getToDoListName() { + return toDoListName; + } + + public void setToDoListName(String toDoListName) { + this.toDoListName = toDoListName; + } + + public String getConfigFilePath() { + return configFilePath; + } + + public void setConfigFilePath(String configFilePath) { + this.configFilePath = configFilePath; + } + + + @Override + public boolean equals(Object other) { + if (other == this){ + return true; + } + if (!(other instanceof Config)){ //this handles null as well. + return false; + } + + Config o = (Config)other; + + return Objects.equals(appTitle, o.appTitle) + && Objects.equals(logLevel, o.logLevel) + && Objects.equals(aliasTableFilePath, o.aliasTableFilePath) + && Objects.equals(userPrefsFilePath, o.userPrefsFilePath) + && Objects.equals(toDoListFilePath, o.toDoListFilePath) + && Objects.equals(toDoListName, o.toDoListName); + } + + @Override + public int hashCode() { + return Objects.hash(appTitle, logLevel, aliasTableFilePath, + userPrefsFilePath, toDoListFilePath, toDoListName); + } + + @Override + public String toString(){ + StringBuilder sb = new StringBuilder(); + sb.append("App title : " + appTitle); + sb.append("\nCurrent log level : " + logLevel); + sb.append("\nAlias Table file location: " + aliasTableFilePath); + sb.append("\nPreference file Location : " + userPrefsFilePath); + sb.append("\nLocal data file location : " + toDoListFilePath); + sb.append("\nToDoList name : " + toDoListName); + return sb.toString(); + } + +} diff --git a/src/main/java/seedu/address/commons/core/EventsCenter.java b/src/main/java/seedu/agendum/commons/core/EventsCenter.java similarity index 92% rename from src/main/java/seedu/address/commons/core/EventsCenter.java rename to src/main/java/seedu/agendum/commons/core/EventsCenter.java index 9652cd5c227b..158194b1aa50 100644 --- a/src/main/java/seedu/address/commons/core/EventsCenter.java +++ b/src/main/java/seedu/agendum/commons/core/EventsCenter.java @@ -1,7 +1,7 @@ -package seedu.address.commons.core; +package seedu.agendum.commons.core; import com.google.common.eventbus.EventBus; -import seedu.address.commons.events.BaseEvent; +import seedu.agendum.commons.events.BaseEvent; import java.util.logging.Logger; diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/agendum/commons/core/GuiSettings.java similarity index 86% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/seedu/agendum/commons/core/GuiSettings.java index e157ac8b8679..116447867e76 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/agendum/commons/core/GuiSettings.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.agendum.commons.core; import java.awt.*; import java.io.Serializable; @@ -64,10 +64,8 @@ public int hashCode() { @Override public String toString(){ - StringBuilder sb = new StringBuilder(); - sb.append("Width : " + windowWidth + "\n"); - sb.append("Height : " + windowHeight + "\n"); - sb.append("Position : " + windowCoordinates); - return sb.toString(); + return "Width : " + windowWidth + "\n" + + "Height : " + windowHeight + "\n" + + "Position : " + windowCoordinates; } } diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/agendum/commons/core/LogsCenter.java similarity index 89% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/seedu/agendum/commons/core/LogsCenter.java index 17939bab4975..4e1cd747c3ed 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/agendum/commons/core/LogsCenter.java @@ -1,7 +1,9 @@ -package seedu.address.commons.core; +package seedu.agendum.commons.core; -import seedu.address.commons.events.BaseEvent; +import seedu.agendum.commons.events.BaseEvent; +import seedu.agendum.commons.util.FileUtil; +import java.io.File; import java.io.IOException; import java.util.logging.*; @@ -15,7 +17,8 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_DIR = "data/logs/"; + private static final String LOG_FILE = "todolist.log"; private static Level currentLogLevel = Level.INFO; private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); private static FileHandler fileHandler; @@ -68,7 +71,8 @@ private static void addFileHandler(Logger logger) { } private static FileHandler createFileHandler() throws IOException { - FileHandler fileHandler = new FileHandler(LOG_FILE, MAX_FILE_SIZE_IN_BYTES, MAX_FILE_COUNT, true); + FileUtil.createDirs(new File(LOG_DIR)); + FileHandler fileHandler = new FileHandler(LOG_DIR+LOG_FILE, MAX_FILE_SIZE_IN_BYTES, MAX_FILE_COUNT, true); fileHandler.setFormatter(new SimpleFormatter()); fileHandler.setLevel(currentLogLevel); return fileHandler; diff --git a/src/main/java/seedu/agendum/commons/core/Messages.java b/src/main/java/seedu/agendum/commons/core/Messages.java new file mode 100644 index 000000000000..66e001e05756 --- /dev/null +++ b/src/main/java/seedu/agendum/commons/core/Messages.java @@ -0,0 +1,18 @@ +package seedu.agendum.commons.core; + +/** + * Container for user visible messages. + */ +public class Messages { + + public static final String MESSAGE_UNKNOWN_COMMAND = "We don't recognise this command"; + public static final String MESSAGE_UNKNOWN_COMMAND_WITH_SUGGESTION = "Did you mean '%1$s'?"; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = + "We don't understand your input. Try\n%1$s"; + public static final String MESSAGE_INVALID_TASK_DISPLAYED_INDEX = "Hey, the task id given is invalid"; + public static final String MESSAGE_DUPLICATE_TASK = "Hey, the task already exists"; + public static final String MESSAGE_MISSING_TASK = "Something is wrong. Try again or restart Agendum?"; + public static final String MESSAGE_TASKS_LISTED_OVERVIEW = "%1$d tasks listed/found!"; + public static final String MESSAGE_ESCAPE_HELP_WINDOW = "Showing search results now, press ESC to go back and" + + " view all tasks"; +} diff --git a/src/main/java/seedu/address/commons/core/UnmodifiableObservableList.java b/src/main/java/seedu/agendum/commons/core/UnmodifiableObservableList.java similarity index 99% rename from src/main/java/seedu/address/commons/core/UnmodifiableObservableList.java rename to src/main/java/seedu/agendum/commons/core/UnmodifiableObservableList.java index 5c25d8647a8d..bdba93b7c5b9 100644 --- a/src/main/java/seedu/address/commons/core/UnmodifiableObservableList.java +++ b/src/main/java/seedu/agendum/commons/core/UnmodifiableObservableList.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.agendum.commons.core; import javafx.beans.InvalidationListener; import javafx.collections.ListChangeListener; diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/agendum/commons/core/Version.java similarity index 96% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/seedu/agendum/commons/core/Version.java index 7ecb85b18f82..9768912a22b5 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/agendum/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.agendum.commons.core; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; @@ -61,7 +61,7 @@ public static Version fromString(String versionString) throws IllegalArgumentExc return new Version(Integer.parseInt(versionMatcher.group(1)), Integer.parseInt(versionMatcher.group(2)), Integer.parseInt(versionMatcher.group(3)), - versionMatcher.group(4) == null ? false : true); + versionMatcher.group(4) != null); } @JsonValue diff --git a/src/main/java/seedu/address/commons/events/BaseEvent.java b/src/main/java/seedu/agendum/commons/events/BaseEvent.java similarity index 90% rename from src/main/java/seedu/address/commons/events/BaseEvent.java rename to src/main/java/seedu/agendum/commons/events/BaseEvent.java index 723a9c69fbd5..4d47420c2feb 100644 --- a/src/main/java/seedu/address/commons/events/BaseEvent.java +++ b/src/main/java/seedu/agendum/commons/events/BaseEvent.java @@ -1,4 +1,4 @@ -package seedu.address.commons.events; +package seedu.agendum.commons.events; public abstract class BaseEvent { diff --git a/src/main/java/seedu/agendum/commons/events/logic/AliasTableChangedEvent.java b/src/main/java/seedu/agendum/commons/events/logic/AliasTableChangedEvent.java new file mode 100644 index 000000000000..791fd454327e --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/logic/AliasTableChangedEvent.java @@ -0,0 +1,25 @@ +//@@author A0133367E +package seedu.agendum.commons.events.logic; + +import java.util.Hashtable; + +import seedu.agendum.commons.events.BaseEvent; + +/** + * Indicate the alias table in {@link seedu.agendum.logic.commands.CommandLibrary} has changed + */ +public class AliasTableChangedEvent extends BaseEvent { + + public final Hashtable aliasTable; + private String message_; + + public AliasTableChangedEvent(String message, Hashtable aliasTable) { + this.aliasTable = aliasTable; + this.message_ = message; + } + + @Override + public String toString() { + return message_; + } +} diff --git a/src/main/java/seedu/agendum/commons/events/model/ChangeSaveLocationEvent.java b/src/main/java/seedu/agendum/commons/events/model/ChangeSaveLocationEvent.java new file mode 100644 index 000000000000..2c196b45ddca --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/model/ChangeSaveLocationEvent.java @@ -0,0 +1,21 @@ +package seedu.agendum.commons.events.model; + +import seedu.agendum.commons.events.BaseEvent; +//@@author A0148095X +/** Indicates a request from model to change the save location of the data file*/ +public class ChangeSaveLocationEvent extends BaseEvent { + + public final String location; + + private String message; + + public ChangeSaveLocationEvent(String saveLocation){ + this.location = saveLocation; + this.message = "Request to change save location to: " + location; + } + + @Override + public String toString() { + return message; + } +} diff --git a/src/main/java/seedu/agendum/commons/events/model/LoadDataRequestEvent.java b/src/main/java/seedu/agendum/commons/events/model/LoadDataRequestEvent.java new file mode 100644 index 000000000000..7b6ae3102bd5 --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/model/LoadDataRequestEvent.java @@ -0,0 +1,19 @@ +package seedu.agendum.commons.events.model; + +import seedu.agendum.commons.events.BaseEvent; + +//@@author A0148095X +/** Indicates a request from model to load data **/ +public class LoadDataRequestEvent extends BaseEvent { + + public final String loadLocation; + + public LoadDataRequestEvent(String loadLocation){ + this.loadLocation = loadLocation; + } + + @Override + public String toString() { + return "Request to load from: " + loadLocation; + } +} diff --git a/src/main/java/seedu/agendum/commons/events/model/ToDoListChangedEvent.java b/src/main/java/seedu/agendum/commons/events/model/ToDoListChangedEvent.java new file mode 100644 index 000000000000..6e5fb8c4f8c9 --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/model/ToDoListChangedEvent.java @@ -0,0 +1,22 @@ +package seedu.agendum.commons.events.model; + +import seedu.agendum.commons.events.BaseEvent; +import seedu.agendum.model.ReadOnlyToDoList; + +/** Indicates the ToDoList in the model has changed*/ +public class ToDoListChangedEvent extends BaseEvent { + + public final ReadOnlyToDoList data; + + private String message; + + public ToDoListChangedEvent(ReadOnlyToDoList data){ + this.data = data; + this.message = "number of tasks " + data.getTaskList().size(); + } + + @Override + public String toString() { + return message; + } +} diff --git a/src/main/java/seedu/agendum/commons/events/storage/DataLoadingExceptionEvent.java b/src/main/java/seedu/agendum/commons/events/storage/DataLoadingExceptionEvent.java new file mode 100644 index 000000000000..61ca2ab7d1ac --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/storage/DataLoadingExceptionEvent.java @@ -0,0 +1,20 @@ +package seedu.agendum.commons.events.storage; + +import seedu.agendum.commons.events.BaseEvent; + +//@@author A0148095X +/** Indicates an exception during a file loading **/ +public class DataLoadingExceptionEvent extends BaseEvent { + + public Exception exception; + + public DataLoadingExceptionEvent(Exception exception) { + this.exception = exception; + } + + @Override + public String toString(){ + return exception.toString(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/storage/DataSavingExceptionEvent.java b/src/main/java/seedu/agendum/commons/events/storage/DataSavingExceptionEvent.java similarity index 78% rename from src/main/java/seedu/address/commons/events/storage/DataSavingExceptionEvent.java rename to src/main/java/seedu/agendum/commons/events/storage/DataSavingExceptionEvent.java index f0a0640ee523..2bd56f58b055 100644 --- a/src/main/java/seedu/address/commons/events/storage/DataSavingExceptionEvent.java +++ b/src/main/java/seedu/agendum/commons/events/storage/DataSavingExceptionEvent.java @@ -1,6 +1,6 @@ -package seedu.address.commons.events.storage; +package seedu.agendum.commons.events.storage; -import seedu.address.commons.events.BaseEvent; +import seedu.agendum.commons.events.BaseEvent; /** * Indicates an exception during a file saving diff --git a/src/main/java/seedu/agendum/commons/events/storage/LoadDataCompleteEvent.java b/src/main/java/seedu/agendum/commons/events/storage/LoadDataCompleteEvent.java new file mode 100644 index 000000000000..4dd40cd7f5c9 --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/storage/LoadDataCompleteEvent.java @@ -0,0 +1,23 @@ +package seedu.agendum.commons.events.storage; + +import seedu.agendum.commons.events.BaseEvent; +import seedu.agendum.model.ReadOnlyToDoList; + +//@@author A0148095X +/** Indicates the ToDoList load request has completed successfully **/ +public class LoadDataCompleteEvent extends BaseEvent { + + public final ReadOnlyToDoList data; + + private String message; + + public LoadDataCompleteEvent(ReadOnlyToDoList data){ + this.data = data; + this.message = "Todo list data load completed. Task list size: " + data.getTaskList().size(); + } + + @Override + public String toString() { + return message; + } +} diff --git a/src/main/java/seedu/agendum/commons/events/ui/CloseHelpWindowRequestEvent.java b/src/main/java/seedu/agendum/commons/events/ui/CloseHelpWindowRequestEvent.java new file mode 100644 index 000000000000..e52997932fa3 --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/ui/CloseHelpWindowRequestEvent.java @@ -0,0 +1,16 @@ +package seedu.agendum.commons.events.ui; + +import seedu.agendum.commons.events.BaseEvent; + +//@@author A0148031R +/** + * An event that requests to close help window + */ +public class CloseHelpWindowRequestEvent extends BaseEvent{ + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/ExitAppRequestEvent.java b/src/main/java/seedu/agendum/commons/events/ui/ExitAppRequestEvent.java similarity index 70% rename from src/main/java/seedu/address/commons/events/ui/ExitAppRequestEvent.java rename to src/main/java/seedu/agendum/commons/events/ui/ExitAppRequestEvent.java index 9af6194543a3..126c706a0499 100644 --- a/src/main/java/seedu/address/commons/events/ui/ExitAppRequestEvent.java +++ b/src/main/java/seedu/agendum/commons/events/ui/ExitAppRequestEvent.java @@ -1,6 +1,6 @@ -package seedu.address.commons.events.ui; +package seedu.agendum.commons.events.ui; -import seedu.address.commons.events.BaseEvent; +import seedu.agendum.commons.events.BaseEvent; /** * Indicates a request for App termination diff --git a/src/main/java/seedu/address/commons/events/ui/IncorrectCommandAttemptedEvent.java b/src/main/java/seedu/agendum/commons/events/ui/IncorrectCommandAttemptedEvent.java similarity index 54% rename from src/main/java/seedu/address/commons/events/ui/IncorrectCommandAttemptedEvent.java rename to src/main/java/seedu/agendum/commons/events/ui/IncorrectCommandAttemptedEvent.java index 991f7ae9fa25..931b939eab5d 100644 --- a/src/main/java/seedu/address/commons/events/ui/IncorrectCommandAttemptedEvent.java +++ b/src/main/java/seedu/agendum/commons/events/ui/IncorrectCommandAttemptedEvent.java @@ -1,14 +1,13 @@ -package seedu.address.commons.events.ui; +package seedu.agendum.commons.events.ui; -import seedu.address.commons.events.BaseEvent; -import seedu.address.logic.commands.Command; +import seedu.agendum.commons.events.BaseEvent; /** * Indicates an attempt to execute an incorrect command */ public class IncorrectCommandAttemptedEvent extends BaseEvent { - public IncorrectCommandAttemptedEvent(Command command) {} + public IncorrectCommandAttemptedEvent() {} @Override public String toString() { diff --git a/src/main/java/seedu/agendum/commons/events/ui/JumpToListRequestEvent.java b/src/main/java/seedu/agendum/commons/events/ui/JumpToListRequestEvent.java new file mode 100644 index 000000000000..0446602c06a2 --- /dev/null +++ b/src/main/java/seedu/agendum/commons/events/ui/JumpToListRequestEvent.java @@ -0,0 +1,25 @@ +package seedu.agendum.commons.events.ui; + +import seedu.agendum.commons.events.BaseEvent; +import seedu.agendum.model.task.Task; + +//@@author A0148031R +/** + * Indicates a request to jump to the list of tasks + */ +public class JumpToListRequestEvent extends BaseEvent { + + public final Task targetTask; + public final boolean hasMultipleTasks; + + public JumpToListRequestEvent(Task task, boolean hasMultipleTasks) { + this.targetTask = task; + this.hasMultipleTasks = hasMultipleTasks; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} \ No newline at end of file diff --git a/src/main/java/seedu/address/commons/events/ui/ShowHelpRequestEvent.java b/src/main/java/seedu/agendum/commons/events/ui/ShowHelpRequestEvent.java similarity index 66% rename from src/main/java/seedu/address/commons/events/ui/ShowHelpRequestEvent.java rename to src/main/java/seedu/agendum/commons/events/ui/ShowHelpRequestEvent.java index a7e40940b2c7..8a19ce0ecb3d 100644 --- a/src/main/java/seedu/address/commons/events/ui/ShowHelpRequestEvent.java +++ b/src/main/java/seedu/agendum/commons/events/ui/ShowHelpRequestEvent.java @@ -1,7 +1,8 @@ -package seedu.address.commons.events.ui; +package seedu.agendum.commons.events.ui; -import seedu.address.commons.events.BaseEvent; +import seedu.agendum.commons.events.BaseEvent; +//@@author A0148031R /** * An event requesting to view the help page. */ diff --git a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java b/src/main/java/seedu/agendum/commons/exceptions/DataConversionException.java similarity index 84% rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java rename to src/main/java/seedu/agendum/commons/exceptions/DataConversionException.java index 1f689bd8e3f9..c71d46bbd420 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java +++ b/src/main/java/seedu/agendum/commons/exceptions/DataConversionException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.agendum.commons.exceptions; /** * Represents an error during conversion of data from one format to another diff --git a/src/main/java/seedu/address/commons/exceptions/DuplicateDataException.java b/src/main/java/seedu/agendum/commons/exceptions/DuplicateDataException.java similarity index 85% rename from src/main/java/seedu/address/commons/exceptions/DuplicateDataException.java rename to src/main/java/seedu/agendum/commons/exceptions/DuplicateDataException.java index 17aa63d5020c..1de975e82d74 100644 --- a/src/main/java/seedu/address/commons/exceptions/DuplicateDataException.java +++ b/src/main/java/seedu/agendum/commons/exceptions/DuplicateDataException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.agendum.commons.exceptions; /** * Signals an error caused by duplicate data where there should be none. diff --git a/src/main/java/seedu/agendum/commons/exceptions/FileDeletionException.java b/src/main/java/seedu/agendum/commons/exceptions/FileDeletionException.java new file mode 100644 index 000000000000..2fbb3ee2a0ae --- /dev/null +++ b/src/main/java/seedu/agendum/commons/exceptions/FileDeletionException.java @@ -0,0 +1,10 @@ +package seedu.agendum.commons.exceptions; + +/** + * Represents an error during deletion of a file + */ +public class FileDeletionException extends Exception { + public FileDeletionException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/seedu/agendum/commons/exceptions/IllegalValueException.java similarity index 88% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/seedu/agendum/commons/exceptions/IllegalValueException.java index a473b43bd86f..d7c9c2743fbb 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/seedu/agendum/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.agendum.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/seedu/agendum/commons/util/AppUtil.java similarity index 81% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/seedu/agendum/commons/util/AppUtil.java index 649cc19aaeda..5bc32a770fdc 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/seedu/agendum/commons/util/AppUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import javafx.scene.image.Image; -import seedu.address.MainApp; +import seedu.agendum.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/agendum/commons/util/CollectionUtil.java similarity index 85% rename from src/main/java/seedu/address/commons/util/CollectionUtil.java rename to src/main/java/seedu/agendum/commons/util/CollectionUtil.java index fde8394f31e5..3490c5662928 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/seedu/agendum/commons/util/CollectionUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import java.util.Collection; import java.util.HashSet; @@ -12,13 +12,13 @@ public class CollectionUtil { /** * Returns true if any of the given items are null. */ - public static boolean isAnyNull(Object... items) { + public static boolean isNotNull(Object... items) { for (Object item : items) { if (item == null) { - return true; + return false; } } - return false; + return true; } @@ -28,7 +28,7 @@ public static boolean isAnyNull(Object... items) { */ public static void assertNoNullElements(Collection items) { assert items != null; - assert !isAnyNull(items); + assert isNotNull(items); } /** diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/seedu/agendum/commons/util/ConfigUtil.java similarity index 84% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/seedu/agendum/commons/util/ConfigUtil.java index af42e03df06c..c777f03bfbe1 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/seedu/agendum/commons/util/ConfigUtil.java @@ -1,8 +1,8 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.exceptions.DataConversionException; import java.io.File; import java.io.IOException; @@ -56,7 +56,9 @@ public static void saveConfig(Config config, String configFilePath) throws IOExc assert config != null; assert configFilePath != null; - FileUtil.serializeObjectToJsonFile(new File(configFilePath), config); + File file = new File(configFilePath); + + FileUtil.createIfMissing(file); + FileUtil.serializeObjectToJsonFile(file, config); } - } diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/agendum/commons/util/FileUtil.java similarity index 67% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/seedu/agendum/commons/util/FileUtil.java index ca8221250de4..e29542edc5e0 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/agendum/commons/util/FileUtil.java @@ -1,15 +1,59 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import seedu.agendum.commons.exceptions.FileDeletionException; + /** - * Writes and reads file + * Writes, reads and deletes file */ public class FileUtil { private static final String CHARSET = "UTF-8"; - + + //@@author A0148095X + public static void deleteFile(String filePath) throws FileDeletionException { + assert StringUtil.isValidPathToFile(filePath); + + File file = new File(filePath); + if (!file.delete()) { + throw new FileDeletionException("Unable to delete file at: " + filePath); + } + } + + /** Even though a path is valid, it might not exist or the user has insufficient privileges.
+ * i.e. J drive is a valid location, but it does not exist. + * + * Creates and deletes an empty file at the path. + * + * @param path must be a valid file path + * @return true if the path is exists and user has sufficient privileges. + */ + public static boolean isPathAvailable(String path) { + + File file = new File(path); + boolean exists = file.exists(); + + try { + createParentDirsOfFile(file); + file.createNewFile(); + } catch (IOException e) { + return false; + } + + if(!exists) { // prevent deleting an existing file + file.delete(); + } + return true; + } + + public static boolean isFileExists(String filePath) { + File file = new File(filePath); + return isFileExists(file); + } + + //@@author public static boolean isFileExists(File file) { return file.exists() && file.isFile(); } diff --git a/src/main/java/seedu/address/commons/util/FxViewUtil.java b/src/main/java/seedu/agendum/commons/util/FxViewUtil.java similarity index 92% rename from src/main/java/seedu/address/commons/util/FxViewUtil.java rename to src/main/java/seedu/agendum/commons/util/FxViewUtil.java index 900efa6bf5c3..7b1f43a8d34b 100644 --- a/src/main/java/seedu/address/commons/util/FxViewUtil.java +++ b/src/main/java/seedu/agendum/commons/util/FxViewUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import javafx.scene.Node; import javafx.scene.layout.AnchorPane; diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/agendum/commons/util/JsonUtil.java similarity index 98% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/seedu/agendum/commons/util/JsonUtil.java index 80b67de5b7e8..11e4cd279084 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/seedu/agendum/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/agendum/commons/util/StringUtil.java similarity index 60% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/seedu/agendum/commons/util/StringUtil.java index 2e94740456a6..3d2edb3e8560 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/agendum/commons/util/StringUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import java.io.PrintWriter; import java.io.StringWriter; @@ -33,4 +33,27 @@ public static String getDetails(Throwable t){ public static boolean isUnsignedInteger(String s){ return s != null && s.matches("^0*[1-9]\\d*$"); } + + //@@author A0148095X + /** + * Checks whether the string matches an approved file path. + *

+ * Examples of valid file paths:
+ * - C:/Program Files (x86)/some-folder/data.xml
+ * - data/todolist.xml
+ * - list.xml
+ *

+ *

+ * Examples of invalid file paths:
+ * - data/.xml
+ * - data/user
+ * - C:/Program /data.xml
+ * - C:/ Files/data.xml
+ *

+ * @param s should be trimmed + * @return true if it is a valid file path + */ + public static boolean isValidPathToFile(String s) { + return s != null && !s.isEmpty() && s.matches("([A-z]\\:)?(\\/?[\\w-_()]+(\\s[\\w-_()])?)+(\\.[\\w]+)"); + } } diff --git a/src/main/java/seedu/address/commons/util/XmlUtil.java b/src/main/java/seedu/agendum/commons/util/XmlUtil.java similarity index 84% rename from src/main/java/seedu/address/commons/util/XmlUtil.java rename to src/main/java/seedu/agendum/commons/util/XmlUtil.java index 2087e7628a1d..3abc2c448f32 100644 --- a/src/main/java/seedu/address/commons/util/XmlUtil.java +++ b/src/main/java/seedu/agendum/commons/util/XmlUtil.java @@ -1,9 +1,12 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; + +import seedu.agendum.storage.XmlSerializableToDoList; + import java.io.File; import java.io.FileNotFoundException; @@ -12,6 +15,18 @@ */ public class XmlUtil { + //@@author A0148095X + public static boolean isFileCorrectFormat(String filePath) { + File file = new File(filePath); + try { + getDataFromFile(file, XmlSerializableToDoList.class); + return true; + } catch (Exception e) { + return false; + } + } + + //@@author /** * Returns the xml data in the file as an object of the specified type. * diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/agendum/logic/Logic.java similarity index 58% rename from src/main/java/seedu/address/logic/Logic.java rename to src/main/java/seedu/agendum/logic/Logic.java index 4df1bc65cabb..970cc35537c3 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/agendum/logic/Logic.java @@ -1,8 +1,8 @@ -package seedu.address.logic; +package seedu.agendum.logic; import javafx.collections.ObservableList; -import seedu.address.logic.commands.CommandResult; -import seedu.address.model.person.ReadOnlyPerson; +import seedu.agendum.logic.commands.CommandResult; +import seedu.agendum.model.task.ReadOnlyTask; /** * API of the Logic component @@ -15,7 +15,7 @@ public interface Logic { */ CommandResult execute(String commandText); - /** Returns the filtered list of persons */ - ObservableList getFilteredPersonList(); + /** Returns the filtered list of tasks */ + ObservableList getFilteredTaskList(); } diff --git a/src/main/java/seedu/agendum/logic/LogicManager.java b/src/main/java/seedu/agendum/logic/LogicManager.java new file mode 100644 index 000000000000..badaab3d5834 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/LogicManager.java @@ -0,0 +1,53 @@ +package seedu.agendum.logic; + +import javafx.collections.ObservableList; +import seedu.agendum.commons.core.ComponentManager; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.logic.commands.AliasCommand; +import seedu.agendum.logic.commands.Command; +import seedu.agendum.logic.commands.CommandLibrary; +import seedu.agendum.logic.commands.CommandResult; +import seedu.agendum.logic.commands.UnaliasCommand; +import seedu.agendum.logic.parser.Parser; +import seedu.agendum.model.Model; +import seedu.agendum.model.task.ReadOnlyTask; + +import java.util.logging.Logger; + +/** + * The main LogicManager of the app. + */ +public class LogicManager extends ComponentManager implements Logic { + private final Logger logger = LogsCenter.getLogger(LogicManager.class); + + private final Model model; + private final Parser parser; + private CommandLibrary commandLibrary; + + public LogicManager(Model model) { + this.model = model; + this.commandLibrary = CommandLibrary.getInstance(); + this.parser = new Parser(this.commandLibrary); + } + + @Override + public CommandResult execute(String commandText) { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + Command command = parser.parseCommand(commandText); + + if (command instanceof AliasCommand) { + ((AliasCommand) command).setData(model, commandLibrary); + } else if (command instanceof UnaliasCommand) { + ((UnaliasCommand) command).setData(model, commandLibrary); + } else { + command.setData(model); + } + return command.execute(); + } + + @Override + public ObservableList getFilteredTaskList() { + return model.getFilteredTaskList(); + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/AddCommand.java b/src/main/java/seedu/agendum/logic/commands/AddCommand.java new file mode 100644 index 000000000000..ce978fec6544 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/AddCommand.java @@ -0,0 +1,102 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.logic.parser.DateTimeUtils; +import seedu.agendum.model.task.*; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * Adds a task to the to do list. + */ +public class AddCommand extends Command { + + public static final String COMMAND_WORD = "add"; + + public static final String COMMAND_FORMAT = "add \n" + + "add by \n" + + "add from to \n" + + "add ''"; + public static final String COMMAND_DESCRIPTION = "adds a task to Agendum"; + public static final String MESSAGE_SUCCESS = "Task added: %1$s"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " Watch Star Wars\n" + + "from 7pm to 9pm"; + + private Task toAdd = null; + + //@@author A0003878Y + /** + * Convenience constructor using name + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public AddCommand(String name) + throws IllegalValueException { + this.toAdd = new Task( + new Name(name) + ); + } + + /** + * Convenience constructor using name, end datetime + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public AddCommand(String name, Optional deadlineDate) + throws IllegalValueException { + this.toAdd = new Task( + new Name(name), + deadlineDate + ); + } + + /** + * Convenience constructor using name, start datetime, end datetime + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public AddCommand(String name, Optional startDateTime, Optional endDateTime) + throws IllegalValueException { + Optional balancedEndDateTime = endDateTime; + if (startDateTime.isPresent() && endDateTime.isPresent()) { + balancedEndDateTime = Optional.of(DateTimeUtils.balanceStartAndEndDateTime(startDateTime.get(), endDateTime.get())); + } + this.toAdd = new Task( + new Name(name), + startDateTime, + balancedEndDateTime + ); + } + + @Override + public CommandResult execute() { + assert model != null; + try { + model.addTask(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } catch (UniqueTaskList.DuplicateTaskException e) { + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } + } + + //@@author + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} + diff --git a/src/main/java/seedu/agendum/logic/commands/AliasCommand.java b/src/main/java/seedu/agendum/logic/commands/AliasCommand.java new file mode 100644 index 000000000000..d882d664102d --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/AliasCommand.java @@ -0,0 +1,72 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.model.Model; + +//@@author A0133367E +/** + * Creates an alias for a reserved command keyword + */ +public class AliasCommand extends Command { + + public static final String COMMAND_WORD = "alias"; + public static final String COMMAND_FORMAT = "alias "; + public static final String COMMAND_DESCRIPTION = "create your shorthand command"; + public static final String MESSAGE_SUCCESS = "New alias <%1$s> created for <%2$s>"; + public static final String MESSAGE_FAILURE_ALIAS_IN_USE = "<%1$s> is already an alias for <%2$s>"; + public static final String MESSAGE_FAILURE_RESERVED_COMMAND_WORD = "<%1$s> is a reserved command word"; + public static final String MESSAGE_FAILURE_NON_ORIGINAL_COMMAND = + "We don't recognise <%1$s> as an Agendum Command"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " mark m"; + + private String aliasValue; + private String aliasKey; + private CommandLibrary commandLibrary; + + public AliasCommand(String aliasKey, String aliasValue) { + this.aliasKey = aliasKey; + this.aliasValue = aliasValue; + } + + public void setData(Model model, CommandLibrary commandLibrary) { + this.model = model; + this.commandLibrary = commandLibrary; + } + + @Override + public CommandResult execute() { + if (commandLibrary.isReservedCommandKeyword(aliasKey)) { + return new CommandResult(String.format( + MESSAGE_FAILURE_RESERVED_COMMAND_WORD, aliasKey)); + } + + if (commandLibrary.isExistingAliasKey(aliasKey)) { + String associatedValue = commandLibrary.getAliasedValue(aliasKey); + return new CommandResult(String.format( + MESSAGE_FAILURE_ALIAS_IN_USE, aliasKey, associatedValue)); + } + + if (!commandLibrary.isReservedCommandKeyword(aliasValue)) { + return new CommandResult(String.format( + MESSAGE_FAILURE_NON_ORIGINAL_COMMAND, aliasValue)); + } + + commandLibrary.addNewAlias(aliasKey, aliasValue); + + return new CommandResult(String.format(MESSAGE_SUCCESS, aliasKey, aliasValue)); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/agendum/logic/commands/Command.java similarity index 53% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/seedu/agendum/logic/commands/Command.java index 7c0ba2fd0161..67d43a8b24d7 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/agendum/logic/commands/Command.java @@ -1,24 +1,44 @@ -package seedu.address.logic.commands; +package seedu.agendum.logic.commands; -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.core.Messages; -import seedu.address.commons.events.ui.IncorrectCommandAttemptedEvent; -import seedu.address.model.Model; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.events.ui.IncorrectCommandAttemptedEvent; +import seedu.agendum.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. */ public abstract class Command { protected Model model; + + /** + * Return the name of this command. + */ + public static String getName() { + return null; + } /** - * Constructs a feedback message to summarise an operation that displayed a listing of persons. + * Return the format of this command. + */ + public static String getFormat() { + return null; + } + /** + * Return the description of this command. + */ + public static String getDescription() { + return null; + } + + /** + * Constructs a feedback message to summarise an operation that displayed a listing of tasks. * * @param displaySize used to generate summary - * @return summary message for persons displayed + * @return summary message for tasks displayed */ - public static String getMessageForPersonListShownSummary(int displaySize) { - return String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, displaySize); + public static String getMessageForTaskListShownSummary(int displaySize) { + return String.format(Messages.MESSAGE_TASKS_LISTED_OVERVIEW, displaySize); } /** @@ -41,6 +61,6 @@ public void setData(Model model) { * Raises an event to indicate an attempt to execute an incorrect command */ protected void indicateAttemptToExecuteIncorrectCommand() { - EventsCenter.getInstance().post(new IncorrectCommandAttemptedEvent(this)); + EventsCenter.getInstance().post(new IncorrectCommandAttemptedEvent()); } } diff --git a/src/main/java/seedu/agendum/logic/commands/CommandLibrary.java b/src/main/java/seedu/agendum/logic/commands/CommandLibrary.java new file mode 100644 index 000000000000..c5363c81d30d --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/CommandLibrary.java @@ -0,0 +1,153 @@ +package seedu.agendum.logic.commands; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import org.reflections.Reflections; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.events.logic.AliasTableChangedEvent; + +/** + * Manages and stores the various Agendum's reserved command keywords and their aliases + */ +public class CommandLibrary { + + private static final Logger logger = LogsCenter.getLogger(CommandLibrary.class); + private static CommandLibrary commandLibrary = new CommandLibrary(); + + private List allCommandWords = new ArrayList(); + + // The keys of the hash table are user-defined aliases + // The values of the has table are Agendum's reserved command keywords + private Hashtable aliasTable = new Hashtable(); + + + + //@@author A0003878Y + private CommandLibrary() { + allCommandWords = new Reflections("seedu.agendum").getSubTypesOf(Command.class) + .stream() + .map(s -> { + try { + return s.getMethod("getName").invoke(null).toString(); + } catch (NullPointerException e) { + return null; + } catch (Exception e) { + logger.severe("Java reflection for Command class failed"); + throw new RuntimeException(); + } + }) + .filter(p -> p != null) // remove nulls + .collect(Collectors.toList()); + } + + //@author + + public static CommandLibrary getInstance() { + return commandLibrary; + } + + public Hashtable getAliasTable() { + return aliasTable; + } + + //@@author A0133367E + /** + * Replace the current commandLibrary's aliasTable with the new aliasTable provided + */ + public void loadAliasTable(Hashtable aliasTable) { + this.aliasTable = aliasTable; + } + + /** + * Returns true if key is already an alias for a command keyword, false otherwise. + */ + public boolean isExistingAliasKey(String key) { + assert key != null; + assert key.equals(key.toLowerCase()); + + return aliasTable.containsKey(key); + } + + /** + * Returns the reserved command keyword that is aliased by key + * + * @param key An existing user-defined alias for a reserved command keyword + * @return The associated reserved command keyword + */ + public String getAliasedValue(String key) { + assert isExistingAliasKey(key); + + return aliasTable.get(key); + } + + /** + * Returns true if value is a reserved command keyword, false otherwise + */ + public boolean isReservedCommandKeyword(String value) { + assert value != null; + assert value.equals(value.toLowerCase()); + + return allCommandWords.contains(value); + } + + /** + * Pre-condition: key is a new unique alias and not a command keyword; + * value is a reserved command keyword. + * Saves the new alias relationship between key and value. + * + * @param key A valid and unique user-defined alias for a reserved command word + * @param value The target reserved command word + */ + public void addNewAlias(String key, String value) { + assert !isExistingAliasKey(key); + assert !isReservedCommandKeyword(key); + assert isReservedCommandKeyword(value); + + aliasTable.put(key, value); + + indicateAliasAdded(key, value); + } + + /** + * Destroy the alias relationship (key can no longer be used in place of command word) + * + * @param key An existing user-defined alias for a reserved command word + */ + public void removeExistingAlias(String key) { + assert isExistingAliasKey(key); + + String value = aliasTable.remove(key); + + indicateAliasRemoved(key, value); + } + + /** + * Raises an event to indicate that an alias has been added to aliasTable in the command library + * + * @param key The new user-defined alias key + * @param value The target reserved command word + */ + private void indicateAliasAdded(String key, String value) { + String message = "Added alias " + key + " for " + value; + EventsCenter eventCenter = EventsCenter.getInstance(); + eventCenter.post(new AliasTableChangedEvent(message, aliasTable)); + } + + /** + * Raises an event to indicate that an alias has been removed from aliasTable in the command library + * + * @param key The alias key to be removed + * @param value The associated reserved command word + */ + private void indicateAliasRemoved(String key, String value) { + String message = "Removed alias " + key + " for " + value; + EventsCenter eventCenter = EventsCenter.getInstance(); + eventCenter.post(new AliasTableChangedEvent(message, aliasTable)); + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/CommandResult.java b/src/main/java/seedu/agendum/logic/commands/CommandResult.java new file mode 100644 index 000000000000..3941d4ea1bf0 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/CommandResult.java @@ -0,0 +1,40 @@ +package seedu.agendum.logic.commands; + +import java.util.List; + +import seedu.agendum.model.task.ReadOnlyTask; + + +/** + * Represents the result of a command execution. + */ +public class CommandResult { + + public final String feedbackToUser; + + public CommandResult(String feedbackToUser) { + assert feedbackToUser != null; + this.feedbackToUser = feedbackToUser; + } + + //@@author A0133367E + /** + * Pre-condition: tasks and originalIndices must be of the same size. + * Returns a string containing each task in tasks + * with the corresponding number in originalIndices prepended + * + * @param tasks List of tasks where each task is be prepended by an index + * @param originalIndices List of corresponding index for each task + * @return String containing all tasks labeled with their corresponding index + */ + public static String tasksToString(List tasks, List originalIndices) { + final StringBuilder builder = new StringBuilder(); + builder.append("\n"); + for (int i = 0; i < tasks.size(); i++) { + builder.append("#").append(originalIndices.get(i)).append(": "); + builder.append(tasks.get(i).getAsText()); + } + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/DeleteCommand.java b/src/main/java/seedu/agendum/logic/commands/DeleteCommand.java new file mode 100644 index 000000000000..c1d87fc17cd5 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/DeleteCommand.java @@ -0,0 +1,79 @@ +package seedu.agendum.logic.commands; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +//@@author A0133367E +/** + * Deletes task(s) identified using their last displayed indices from the task listing. + */ +public class DeleteCommand extends Command { + + public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_FORMAT = "delete "; + public static final String COMMAND_DESCRIPTION = "delete task(s) from Agendum"; + public static final String MESSAGE_DELETE_TASK_SUCCESS = "Deleted Task(s): %1$s"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "(The id must be a positive number)\n" + + "Example: " + COMMAND_WORD + " 7 10-11"; + + private ArrayList targetIndexes; + private ArrayList tasksToDelete; + + + public DeleteCommand(Set targetIndexes) { + this.targetIndexes = new ArrayList(targetIndexes); + Collections.sort(this.targetIndexes); + this.tasksToDelete = new ArrayList(); + } + + @Override + public CommandResult execute() { + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (isAnyIndexInvalid(lastShownList)) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + for (int targetIndex: targetIndexes) { + ReadOnlyTask taskToDelete = lastShownList.get(targetIndex - 1); + tasksToDelete.add(taskToDelete); + } + + try { + model.deleteTasks(tasksToDelete); + } catch (TaskNotFoundException pnfe) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } + + return new CommandResult(String.format(MESSAGE_DELETE_TASK_SUCCESS, + CommandResult.tasksToString(tasksToDelete, targetIndexes))); + } + + private boolean isAnyIndexInvalid(UnmodifiableObservableList lastShownList) { + return targetIndexes.stream().anyMatch(index -> index > lastShownList.size()); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/ExitCommand.java b/src/main/java/seedu/agendum/logic/commands/ExitCommand.java new file mode 100644 index 000000000000..db71921c7d71 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/ExitCommand.java @@ -0,0 +1,34 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.events.ui.ExitAppRequestEvent; + +/** + * Terminates the program. + */ +public class ExitCommand extends Command { + + // COMMAND_WORD, COMMAND_FORMAT, COMMAND_DESCRIPTION are for display in help window + public static final String COMMAND_WORD = "exit"; + public static final String COMMAND_FORMAT = "exit"; + public static final String COMMAND_DESCRIPTION = "exit Agendum"; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Agendum as requested ..."; + + @Override + public CommandResult execute() { + EventsCenter.getInstance().post(new ExitAppRequestEvent()); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/agendum/logic/commands/FindCommand.java b/src/main/java/seedu/agendum/logic/commands/FindCommand.java new file mode 100644 index 000000000000..9656291d5744 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/FindCommand.java @@ -0,0 +1,44 @@ +package seedu.agendum.logic.commands; + +import java.util.Set; + +/** + * Finds and lists all tasks in to do list whose name contains any of the argument keywords. + * Keyword matching is case sensitive. + */ +public class FindCommand extends Command { + + // COMMAND_WORD, COMMAND_FORMAT, COMMAND_DESCRIPTION are for display in help window + public static final String COMMAND_WORD = "find"; + public static final String COMMAND_FORMAT= "find "; + public static final String COMMAND_DESCRIPTION = "search for task(s) matching any of the keywords"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " star wars"; + + private Set keywords = null; + + public FindCommand(Set keywords) { + this.keywords = keywords; + } + + @Override + public CommandResult execute() { + model.updateFilteredTaskList(keywords); + return new CommandResult(getMessageForTaskListShownSummary(model.getFilteredTaskList().size())); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/HelpCommand.java b/src/main/java/seedu/agendum/logic/commands/HelpCommand.java new file mode 100644 index 000000000000..86505a944cf1 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/HelpCommand.java @@ -0,0 +1,38 @@ +package seedu.agendum.logic.commands; + + +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.events.ui.ShowHelpRequestEvent; + +//@@author A0148031R +/** + * Format full help instructions for every command for display. + */ +public class HelpCommand extends Command { + + // COMMAND_WORD, COMMAND_FORMAT, COMMAND_DESCRIPTION are for display in help window + public static final String COMMAND_WORD = "help"; + public static final String COMMAND_FORMAT = "help"; + public static final String COMMAND_DESCRIPTION = "view a summary of Agendum commands"; + public static final String MESSAGE_USAGE = COMMAND_WORD + "- " + + COMMAND_DESCRIPTION; + public static final String SHOWING_HELP_MESSAGE = "Opened help window."; + + @Override + public CommandResult execute() { + EventsCenter.getInstance().post(new ShowHelpRequestEvent()); + return new CommandResult(SHOWING_HELP_MESSAGE); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/address/logic/commands/IncorrectCommand.java b/src/main/java/seedu/agendum/logic/commands/IncorrectCommand.java similarity index 65% rename from src/main/java/seedu/address/logic/commands/IncorrectCommand.java rename to src/main/java/seedu/agendum/logic/commands/IncorrectCommand.java index 491d9cb9da35..ea916ddc345f 100644 --- a/src/main/java/seedu/address/logic/commands/IncorrectCommand.java +++ b/src/main/java/seedu/agendum/logic/commands/IncorrectCommand.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands; +package seedu.agendum.logic.commands; /** @@ -18,5 +18,17 @@ public CommandResult execute() { return new CommandResult(feedbackToUser); } + public static String getName() { + return null; + } + + public static String getFormat() { + return null; + } + + public static String getDescription() { + return null; + } + } diff --git a/src/main/java/seedu/agendum/logic/commands/ListCommand.java b/src/main/java/seedu/agendum/logic/commands/ListCommand.java new file mode 100644 index 000000000000..daeca0fed00c --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/ListCommand.java @@ -0,0 +1,32 @@ +package seedu.agendum.logic.commands; + + +/** + * Lists all tasks in the to do list to the user. + */ +public class ListCommand extends Command { + + // COMMAND_WORD, COMMAND_FORMAT, COMMAND_DESCRIPTION are for display in help window + public static final String COMMAND_WORD = "list"; + public static final String COMMAND_FORMAT = "list \n"; + public static final String COMMAND_DESCRIPTION = "list all your tasks"; + public static final String MESSAGE_SUCCESS = "Listed all tasks"; + + @Override + public CommandResult execute() { + model.updateFilteredListToShowAll(); + return new CommandResult(MESSAGE_SUCCESS); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/agendum/logic/commands/LoadCommand.java b/src/main/java/seedu/agendum/logic/commands/LoadCommand.java new file mode 100644 index 000000000000..8cab870fdcef --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/LoadCommand.java @@ -0,0 +1,79 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.commons.util.XmlUtil; + +//@@author A0148095X +/** Allow the user to load a file in the correct todolist format **/ +public class LoadCommand extends Command { + + public static final String COMMAND_WORD = "load"; + public static final String COMMAND_FORMAT = "load "; + public static final String COMMAND_DESCRIPTION = "loads task data from the specified location"; + + public static final String MESSAGE_SUCCESS = "Data successfully loaded from: %1$s"; + public static final String MESSAGE_PATH_INVALID = "The specified path to file is invalid: %1$s"; + public static final String MESSAGE_FILE_DOES_NOT_EXIST = "The specified file does not exist: %1$s"; + public static final String MESSAGE_FILE_WRONG_FORMAT = "The specified file is in the wrong format: %1$s"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + "agendum/todolist.xml"; + + private String pathToFile; + + public LoadCommand(String pathToFile) { + this.pathToFile = pathToFile.trim(); + } + + @Override + public CommandResult execute() { + assert pathToFile != null; + + if(!isValidPathToFile()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(String.format(MESSAGE_PATH_INVALID, pathToFile)); + } + + if(!isFileExists()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(String.format(MESSAGE_FILE_DOES_NOT_EXIST, pathToFile)); + } + + if(!isFileCorrectFormat()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(String.format(MESSAGE_FILE_WRONG_FORMAT, pathToFile)); + } + + model.loadFromLocation(pathToFile); + return new CommandResult(String.format(MESSAGE_SUCCESS, pathToFile)); + } + + private boolean isFileCorrectFormat() { + return XmlUtil.isFileCorrectFormat(pathToFile); + } + + private boolean isValidPathToFile() { + return StringUtil.isValidPathToFile(pathToFile); + } + + private boolean isFileExists() { + return FileUtil.isFileExists(pathToFile); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + + +} diff --git a/src/main/java/seedu/agendum/logic/commands/MarkCommand.java b/src/main/java/seedu/agendum/logic/commands/MarkCommand.java new file mode 100644 index 000000000000..a7d45e775bc8 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/MarkCommand.java @@ -0,0 +1,82 @@ +package seedu.agendum.logic.commands; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.UniqueTaskList.DuplicateTaskException; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +//@@author A0133367E +/** + * Mark task(s) identified using their last displayed indices in the task listing. + */ +public class MarkCommand extends Command { + + public static final String COMMAND_WORD = "mark"; + public static final String COMMAND_FORMAT = "mark "; + public static final String COMMAND_DESCRIPTION = "mark task(s) as completed"; + + public static final String MESSAGE_MARK_TASK_SUCCESS = "Marked Task(s)!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "(The id must be a positive number)\n" + + "Example: " + COMMAND_WORD + " 1 3 5-6"; + + private ArrayList targetIndexes; + private ArrayList tasksToMark; + + + public MarkCommand(Set targetIndexes) { + this.targetIndexes = new ArrayList(targetIndexes); + Collections.sort(this.targetIndexes); + this.tasksToMark = new ArrayList(); + } + + @Override + public CommandResult execute() { + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (isAnyIndexInvalid(lastShownList)) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + for (int targetIndex: targetIndexes) { + ReadOnlyTask taskToMark = lastShownList.get(targetIndex - 1); + tasksToMark.add(taskToMark); + } + + try { + model.markTasks(tasksToMark); + } catch (TaskNotFoundException pnfe) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } catch (DuplicateTaskException pnfe) { + model.resetDataToLastSavedList(); + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } + + return new CommandResult(MESSAGE_MARK_TASK_SUCCESS); + } + + private boolean isAnyIndexInvalid(UnmodifiableObservableList lastShownList) { + return targetIndexes.stream().anyMatch(index -> index > lastShownList.size()); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/agendum/logic/commands/RenameCommand.java b/src/main/java/seedu/agendum/logic/commands/RenameCommand.java new file mode 100644 index 000000000000..06441ab6a385 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/RenameCommand.java @@ -0,0 +1,75 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.*; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +//@@author A0133367E +/** + * Renames the target task in the task listing. + */ +public class RenameCommand extends Command { + + public static final String COMMAND_WORD = "rename"; + public static final String COMMAND_FORMAT = "rename "; + public static final String COMMAND_DESCRIPTION = "update the name of a task"; + public static final String MESSAGE_SUCCESS = "Task renamed: %1$s"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " 2 Watch Star Trek"; + + private int targetIndex; + private Name newTaskName; + + /** + * Constructor for rename command + * @throws IllegalValueException if the name is invalid + */ + public RenameCommand(int targetIndex, String name) throws IllegalValueException { + this.targetIndex = targetIndex; + this.newTaskName = new Name(name); + } + + @Override + public CommandResult execute() { + assert model != null; + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + ReadOnlyTask taskToRename = lastShownList.get(targetIndex - 1); + + try { + Task renamedTask = new Task(taskToRename); + renamedTask.setName(newTaskName); + model.updateTask(taskToRename, renamedTask); + } catch (UniqueTaskList.DuplicateTaskException e) { + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } catch (TaskNotFoundException e) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, newTaskName)); + + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/ScheduleCommand.java b/src/main/java/seedu/agendum/logic/commands/ScheduleCommand.java new file mode 100644 index 000000000000..4950b033ed72 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/ScheduleCommand.java @@ -0,0 +1,89 @@ +package seedu.agendum.logic.commands; + +import java.time.LocalDateTime; +import java.util.Optional; + +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.logic.parser.DateTimeUtils; +import seedu.agendum.model.task.*; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +/** + * Reschedules a task in the to do list. + */ +public class ScheduleCommand extends Command { + + public static final String COMMAND_WORD = "schedule"; + public static final String COMMAND_FORMAT = "schedule \n" + + "schedule by \n" + + "schedule from to "; + public static final String COMMAND_DESCRIPTION = "update the time of a task"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "(The id must be a positive number)\n" + + "Example: " + COMMAND_WORD + " 2 from 7am to 9am"; + + public static final String MESSAGE_SUCCESS = "Task rescheduled: %1$s"; + + public int targetIndex = -1; + public Optional newStartDateTime = Optional.empty(); + public Optional newEndDateTime = Optional.empty(); + + //@@author A0003878Y + public ScheduleCommand(int targetIndex, Optional startTime, + Optional endTime) { + Optional balancedEndTime = endTime; + if (startTime.isPresent() && endTime.isPresent()) { + balancedEndTime = Optional.of(DateTimeUtils.balanceStartAndEndDateTime(startTime.get(), endTime.get())); + } + this.targetIndex = targetIndex; + this.newStartDateTime = startTime; + this.newEndDateTime = balancedEndTime; + } + + @Override + public CommandResult execute() { + assert model != null; + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + ReadOnlyTask taskToSchedule = lastShownList.get(targetIndex - 1); + + Task updatedTask = new Task(taskToSchedule); + updatedTask.setStartDateTime(newStartDateTime); + updatedTask.setEndDateTime(newEndDateTime); + + try { + model.updateTask(taskToSchedule, updatedTask); + } catch (UniqueTaskList.DuplicateTaskException e) { + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } catch (TaskNotFoundException e) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, updatedTask)); + } + + //@@author + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/StoreCommand.java b/src/main/java/seedu/agendum/logic/commands/StoreCommand.java new file mode 100644 index 000000000000..49641ef82177 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/StoreCommand.java @@ -0,0 +1,87 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.commons.util.StringUtil; + +//@@author A0148095X +/** Allow the user to specify a folder as the data storage location **/ +public class StoreCommand extends Command { + + public static final String COMMAND_WORD = "store"; + public static final String COMMAND_FORMAT = "store "; + public static final String COMMAND_DESCRIPTION = "stores task data at specified location"; + public static final String COMMAND_EXAMPLE = "store agendum/todolist.xml"; + + public static final String MESSAGE_SUCCESS = "New save location: %1$s"; + public static final String MESSAGE_LOCATION_DEFAULT = "Save location set to default: %1$s"; + + public static final String MESSAGE_LOCATION_INACCESSIBLE = "The specified location is inaccessible; try running Agendum as administrator."; + public static final String MESSAGE_FILE_EXISTS = "The specified file exists; would you like to use LOAD instead?"; + public static final String MESSAGE_PATH_WRONG_FORMAT = "The specified path is in the wrong format. Example: " + COMMAND_EXAMPLE; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + "agendum/todolist.xml"; + + private String pathToFile; + + public StoreCommand(String location) { + this.pathToFile = location.trim(); + } + + @Override + public CommandResult execute() { + assert pathToFile != null; + + if(pathToFile.equalsIgnoreCase("default")) { // for debug + String defaultLocation = Config.DEFAULT_SAVE_LOCATION; + model.changeSaveLocation(defaultLocation); + return new CommandResult(String.format(MESSAGE_LOCATION_DEFAULT, defaultLocation)); + } + + if(isFileExists()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(MESSAGE_FILE_EXISTS); + } + + if(!isPathCorrectFormat()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(MESSAGE_PATH_WRONG_FORMAT); + } + + if(!isPathAvailable()) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(MESSAGE_LOCATION_INACCESSIBLE); + } + + model.changeSaveLocation(pathToFile); + return new CommandResult(String.format(MESSAGE_SUCCESS, pathToFile)); + } + + private boolean isPathCorrectFormat() { + return StringUtil.isValidPathToFile(pathToFile); + } + + private boolean isPathAvailable() { + return FileUtil.isPathAvailable(pathToFile); + } + + private boolean isFileExists() { + return FileUtil.isFileExists(pathToFile); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } + +} diff --git a/src/main/java/seedu/agendum/logic/commands/SyncCommand.java b/src/main/java/seedu/agendum/logic/commands/SyncCommand.java new file mode 100644 index 000000000000..42e4c977dbf9 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/SyncCommand.java @@ -0,0 +1,63 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.commons.exceptions.IllegalValueException; + +public class SyncCommand extends Command { + // COMMAND_WORD, COMMAND_FORMAT, COMMAND_DESCRIPTION are for display in help window + public static final String COMMAND_WORD = "sync"; + private static final String COMMAND_FORMAT = "sync "; + private static final String COMMAND_DESCRIPTION = "Turn syncing on or off"; + private static final String MESSAGE_USAGE = COMMAND_WORD + "- " + + COMMAND_DESCRIPTION; + + public static final String SYNC_ON = "on"; + public static final String SYNC_OFF = "off"; + + public static final String SYNC_ON_MESSAGE = "Google Calendar Sync is on"; + public static final String SYNC_OFF_MESSAGE = "Google Calendar Sync is off"; + + public static final String MESSAGE_WRONG_OPTION = "Invalid option for sync."; + + private boolean syncOption; + + //@@author A0003878Y + /** + * Convenience constructor using name + * + * @throws IllegalValueException if any of the raw values are invalid + */ + public SyncCommand(String option) throws IllegalValueException { + + if (option.trim().equalsIgnoreCase(SYNC_ON)) { + syncOption = true; + } else if (option.trim().equalsIgnoreCase(SYNC_OFF)) { + syncOption = false; + } else { + throw new IllegalValueException(MESSAGE_WRONG_OPTION); + } + } + + @Override + public CommandResult execute() { + if (syncOption) { + model.activateModelSyncing(); + return new CommandResult(SYNC_ON_MESSAGE); + } else { + model.deactivateModelSyncing(); + return new CommandResult(SYNC_OFF_MESSAGE); + } + } + + //@@author + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/agendum/logic/commands/UnaliasCommand.java b/src/main/java/seedu/agendum/logic/commands/UnaliasCommand.java new file mode 100644 index 000000000000..d8c1b108d762 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/UnaliasCommand.java @@ -0,0 +1,61 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.model.Model; + +//@@author A0133367E +/** + * Create an alias for a reserved command keyword + */ +public class UnaliasCommand extends Command { + + public static final String COMMAND_WORD = "unalias"; + public static final String COMMAND_FORMAT = "unalias "; + public static final String COMMAND_DESCRIPTION = "remove a shorthand command"; + + public static final String MESSAGE_SUCCESS = "Removed alias <%1$s>"; + public static final String MESSAGE_FAILURE_NO_ALIAS_KEY = "The alias <%1$s> does not exist"; + public static final String MESSAGE_FAILURE_RESERVED_COMMAND_WORD = "<%1$s> is a reserved command word"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "Example: " + COMMAND_WORD + " m\n" + + "(if m is aliased to mark)"; + + private String aliasKey; + private CommandLibrary commandLibrary; + + public UnaliasCommand(String aliasKey) { + this.aliasKey = aliasKey; + } + + public void setData(Model model, CommandLibrary commandLibrary) { + this.model = model; + this.commandLibrary = commandLibrary; + } + + @Override + public CommandResult execute() { + if (commandLibrary.isReservedCommandKeyword(aliasKey)) { + return new CommandResult(String.format(MESSAGE_FAILURE_RESERVED_COMMAND_WORD, aliasKey)); + } + + if (!commandLibrary.isExistingAliasKey(aliasKey)) { + return new CommandResult(String.format(MESSAGE_FAILURE_NO_ALIAS_KEY, aliasKey)); + } + + commandLibrary.removeExistingAlias(aliasKey); + return new CommandResult(String.format(MESSAGE_SUCCESS, aliasKey)); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/agendum/logic/commands/UndoCommand.java b/src/main/java/seedu/agendum/logic/commands/UndoCommand.java new file mode 100644 index 000000000000..3c3df8e641d2 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/UndoCommand.java @@ -0,0 +1,42 @@ +package seedu.agendum.logic.commands; + +import seedu.agendum.model.ModelManager.NoPreviousListFoundException; + +//@@author A0133367E +/** + * Undo the last change to the to-do list + */ +public class UndoCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + public static final String COMMAND_FORMAT = "undo"; + public static final String COMMAND_DESCRIPTION = "undo the last change to your to-do list"; + + public static final String MESSAGE_SUCCESS = "Previous change undone!"; + public static final String MESSAGE_FAILURE = "Nothing to undo!"; + + @Override + public CommandResult execute() { + assert model != null; + + try { + model.restorePreviousToDoList(); + } catch (NoPreviousListFoundException nplfe) { + return new CommandResult(MESSAGE_FAILURE); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} \ No newline at end of file diff --git a/src/main/java/seedu/agendum/logic/commands/UnmarkCommand.java b/src/main/java/seedu/agendum/logic/commands/UnmarkCommand.java new file mode 100644 index 000000000000..37a4c6e0b13a --- /dev/null +++ b/src/main/java/seedu/agendum/logic/commands/UnmarkCommand.java @@ -0,0 +1,82 @@ +package seedu.agendum.logic.commands; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.UniqueTaskList.DuplicateTaskException; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +//@@author A0133367E +/** + * Unmark task(s) identified using their last displayed indices in the task listing. + */ +public class UnmarkCommand extends Command { + + public static final String COMMAND_WORD = "unmark"; + public static final String COMMAND_FORMAT = "unmark "; + public static final String COMMAND_DESCRIPTION = "unmark task(s) from completed"; + + public static final String MESSAGE_UNMARK_TASK_SUCCESS = "Unmarked Task(s)!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " - " + + COMMAND_DESCRIPTION + "\n" + + COMMAND_FORMAT + "\n" + + "(The id must be a positive number)\n" + + "Example: " + COMMAND_WORD + " 11-13 15"; + + private ArrayList targetIndexes; + private ArrayList tasksToUnmark; + + //@@author A0133367E + public UnmarkCommand(Set targetIndexes) { + this.targetIndexes = new ArrayList<>(targetIndexes); + Collections.sort(this.targetIndexes); + this.tasksToUnmark = new ArrayList<>(); + } + + @Override + public CommandResult execute() { + + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + + if (isAnyIndexInvalid(lastShownList)) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + for (int targetIndex: targetIndexes) { + ReadOnlyTask taskToUnmark = lastShownList.get(targetIndex - 1); + tasksToUnmark.add(taskToUnmark); + } + + try { + model.unmarkTasks(tasksToUnmark); + } catch (TaskNotFoundException pnfe) { + return new CommandResult(Messages.MESSAGE_MISSING_TASK); + } catch (DuplicateTaskException pnfe) { + model.resetDataToLastSavedList(); + return new CommandResult(Messages.MESSAGE_DUPLICATE_TASK); + } + + return new CommandResult(MESSAGE_UNMARK_TASK_SUCCESS); + } + + private boolean isAnyIndexInvalid(UnmodifiableObservableList lastShownList) { + return targetIndexes.stream().anyMatch(index -> index > lastShownList.size()); + } + + public static String getName() { + return COMMAND_WORD; + } + + public static String getFormat() { + return COMMAND_FORMAT; + } + + public static String getDescription() { + return COMMAND_DESCRIPTION; + } +} diff --git a/src/main/java/seedu/agendum/logic/parser/DateTimeUtils.java b/src/main/java/seedu/agendum/logic/parser/DateTimeUtils.java new file mode 100644 index 000000000000..80d8976795f7 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/parser/DateTimeUtils.java @@ -0,0 +1,66 @@ +package seedu.agendum.logic.parser; + +import com.joestelmach.natty.DateGroup; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +//@@author A0003878Y + +/** + * Utilities for DateTime parsing + */ +public class DateTimeUtils { + + /** + * Parses input string into LocalDateTime objects using Natural Language Parsing + * @param input natural language date time string + * @return Optional is null if input coult not be parsed + */ + public static Optional parseNaturalLanguageDateTimeString(String input) { + if(input == null || input.isEmpty()) { + return Optional.empty(); + } + // Referring to natty's Parser Class using its full path because of the namespace collision with our Parser class. + com.joestelmach.natty.Parser parser = new com.joestelmach.natty.Parser(); + List groups = parser.parse(input); + + if (groups.size() <= 0) { + // Nothing found + return Optional.empty(); + } + + DateGroup dateGroup = (DateGroup) groups.get(0); + + if (dateGroup.getDates().size() < 0) { + return Optional.empty(); + } + + Date date = dateGroup.getDates().get(0); + + LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + return Optional.ofNullable(localDateTime); + } + + /** + * Takes two LocalDateTime and balances by ensuring that the latter DateTime is gaurenteed to be later + * than the former DateTime + * @param startDateTime + * @param endDateTime + * @return endDateTime that is now balanced + */ + public static LocalDateTime balanceStartAndEndDateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) { + LocalDateTime newEndDateTime = endDateTime; + while (startDateTime.compareTo(newEndDateTime) >= 1) { + newEndDateTime = newEndDateTime.plusDays(1); + } + return newEndDateTime; + } + + public static boolean containsTime(String input) { + return parseNaturalLanguageDateTimeString(input).isPresent(); + } +} diff --git a/src/main/java/seedu/agendum/logic/parser/EditDistanceCalculator.java b/src/main/java/seedu/agendum/logic/parser/EditDistanceCalculator.java new file mode 100644 index 000000000000..c60c36755284 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/parser/EditDistanceCalculator.java @@ -0,0 +1,121 @@ +package seedu.agendum.logic.parser; + +import org.reflections.Reflections; +import seedu.agendum.commons.core + .LogsCenter; +import seedu.agendum.logic.commands.Command; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.logging.Logger; + +//@@author A0003878Y + +/** + * A static class for calculating levenshtein distance between two strings + */ +public class EditDistanceCalculator { + + private static final Logger logger = LogsCenter.getLogger(EditDistanceCalculator.class); + private static final int EDIT_DISTANCE_THRESHOLD = 3; + + /** + * Attempts to find the 'closest' command for an input String + * @param input user inputted command + * @return Optional string that's the closest command to input. Null if not found. + */ + public static Optional closestCommandMatch(String input) { + final String[] bestCommand = {""}; + final int[] bestCommandDistance = {Integer.MAX_VALUE}; + + Consumer consumer = (commandWord) -> { + int commandWordDistance = distance(input, commandWord); + + if (commandWordDistance < bestCommandDistance[0]) { + bestCommand[0] = commandWord; + bestCommandDistance[0] = commandWordDistance; + } + }; + executeOnAllCommands(consumer); + + if (bestCommandDistance[0] < EDIT_DISTANCE_THRESHOLD) { + return Optional.of(bestCommand[0]); + } else { + return Optional.empty(); + } + } + + /** + * Attempts to 'complete' the input String into an actual command + * @param input user inputted command + * @return Optional string that's command that best completes the input. If input matches more than + * one command, null ire returned. Null is also returned if a command is not found. + */ + public static Optional findCommandCompletion(String input) { + ArrayList matchedCommands = new ArrayList<>(); + + Consumer consumer = (commandWord) -> { + if (commandWord.startsWith(input)) { + matchedCommands.add(commandWord); + } + }; + executeOnAllCommands(consumer); + + if (matchedCommands.size() == 1) { + return Optional.of(matchedCommands.get(0)); + } else { + return Optional.empty(); + } + } + + /** + * A higher order method that takes in an operation to perform on all Commands using + * Java reflection and functional programming paradigm. + * @param f A closure that takes a String as input that executes on all Commands. + */ + private static void executeOnAllCommands(Consumer f) { + new Reflections("seedu.agendum").getSubTypesOf(Command.class) + .stream() + .map(s -> { + try { + return s.getMethod("getName").invoke(null).toString(); + } catch (NullPointerException e) { + return ""; // Suppress this exception are we expect some Commands to not conform to getName() + } catch (Exception e) { + logger.severe("Java reflection for Command class failed"); + throw new RuntimeException(); + } + }) + .filter(p -> p != "") // remove empty + .forEach(f); // execute given lambda on each nonnull String. + } + + + /** + * Calculates levenshtein distnace between two strings. + * Code from https://rosettacode.org/wiki/Levenshtein_distance#Java + * @param a + * @param b + * @return + */ + private static int distance(String a, String b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + int [] costs = new int [b.length() + 1]; + for (int j = 0; j < costs.length; j++) + costs[j] = j; + for (int i = 1; i <= a.length(); i++) { + costs[0] = i; + int nw = i - 1; + for (int j = 1; j <= b.length(); j++) { + int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1); + nw = costs[j]; + costs[j] = cj; + } + } + return costs[b.length()]; + } + +} diff --git a/src/main/java/seedu/agendum/logic/parser/Parser.java b/src/main/java/seedu/agendum/logic/parser/Parser.java new file mode 100644 index 000000000000..c594b0647b23 --- /dev/null +++ b/src/main/java/seedu/agendum/logic/parser/Parser.java @@ -0,0 +1,510 @@ +package seedu.agendum.logic.parser; + +import seedu.agendum.logic.commands.*; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.commons.exceptions.IllegalValueException; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static seedu.agendum.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.agendum.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.agendum.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND_WITH_SUGGESTION; + +/** + * Parses user input. + */ +public class Parser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + private static final Pattern TASK_INDEX_ARGS_FORMAT = Pattern.compile("(?.+)"); + + private static final Pattern TASK_INDEXES_ARGS_FORMAT = Pattern.compile("((\\d+|\\d+-\\d+)?[ ]*,?[ ]*)+"); + + private static final Pattern KEYWORDS_ARGS_FORMAT = + Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace + + private static final Pattern RENAME_ARGS_FORMAT = Pattern.compile("(?\\d+)\\s+(?.+)"); + + private static final Pattern ALIAS_ARGS_FORMAT = Pattern.compile( + "(?[\\p{Alnum}]+)\\s+(?[\\p{Alnum}]+)"); + + private static final Pattern UNALIAS_ARGS_FORMAT = Pattern.compile("(?[\\p{Alnum}]+)"); + + //@@author A0003878Y + private static final Pattern QUOTATION_FORMAT = Pattern.compile("\'([^\']*)\'"); + private static final Pattern ADD_SCHEDULE_ARGS_FORMAT = Pattern.compile("(?:.+?(?=(?:(?:(?i)by|from|to)\\s|$)))+?"); + + private static final String ARGS_FROM = "from"; + private static final String ARGS_BY = "by"; + private static final String ARGS_TO = "to"; + private static final String FILLER_WORD = "FILLER "; + private static final String SINGLE_QUOTE = "\'"; + + private static final String[] TIME_TOKENS = new String[] { ARGS_FROM, ARGS_TO, ARGS_BY }; + + private CommandLibrary commandLibrary; + + //@@author + + public Parser(CommandLibrary commandLibrary) { + this.commandLibrary = commandLibrary; + } + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + */ + public Command parseCommand(String userInput) { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + String commandWord = matcher.group("commandWord").toLowerCase(); + if (commandLibrary.isExistingAliasKey(commandWord)) { + commandWord = commandLibrary.getAliasedValue(commandWord); + } + + final String arguments = matcher.group("arguments"); + + switch (commandWord) { + + case AddCommand.COMMAND_WORD : + return prepareAdd(arguments); + + case DeleteCommand.COMMAND_WORD : + return prepareDelete(arguments); + + case FindCommand.COMMAND_WORD : + return prepareFind(arguments); + + case ListCommand.COMMAND_WORD : + return new ListCommand(); + + case RenameCommand.COMMAND_WORD : + return prepareRename(arguments); + + case MarkCommand.COMMAND_WORD : + return prepareMark(arguments); + + case ScheduleCommand.COMMAND_WORD : + return prepareSchedule(arguments); + + case UnmarkCommand.COMMAND_WORD : + return prepareUnmark(arguments); + + case UndoCommand.COMMAND_WORD : + return new UndoCommand(); + + case AliasCommand.COMMAND_WORD : + return prepareAlias(arguments); + + case UnaliasCommand.COMMAND_WORD : + return prepareUnalias(arguments); + + case ExitCommand.COMMAND_WORD : + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD : + return new HelpCommand(); + + case StoreCommand.COMMAND_WORD : + return new StoreCommand(arguments); + + case LoadCommand.COMMAND_WORD : + return new LoadCommand(arguments); + + case SyncCommand.COMMAND_WORD: { + return prepareSync(arguments); + } + + default: + //@@author A0003878Y + Optional alternativeCommand = EditDistanceCalculator.closestCommandMatch(commandWord); + if (alternativeCommand.isPresent()) { + return new IncorrectCommand(String.format(MESSAGE_UNKNOWN_COMMAND_WITH_SUGGESTION, alternativeCommand.get())); + } else { + return new IncorrectCommand(MESSAGE_UNKNOWN_COMMAND); + } + } + } + + //@@author A0003878Y + /** + * Parses arguments in the context of the add task command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareAdd(String args) { + + // Create title and dateTimeMap + StringBuilder titleBuilder = new StringBuilder(); + HashMap> dateTimeMap = new HashMap<>(); + + // Check for quotation in args. If so, they're set as title + Optional quotationCheck = checkForQuotation(args); + if (quotationCheck.isPresent()) { + titleBuilder.append(quotationCheck.get().replace(SINGLE_QUOTE,"")); + args = FILLER_WORD + args.replace(quotationCheck.get(),""); // This will get removed later by regex + } + + // Start parsing for datetime in args + Matcher matcher = ADD_SCHEDULE_ARGS_FORMAT.matcher(args.trim()); + + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + try { + matcher.reset(); + matcher.find(); + if (titleBuilder.length() == 0) { + titleBuilder.append(matcher.group(0)); + } + + // Run this function on all matched groups + BiConsumer consumer = (matchedGroup, token) -> { + String time = matchedGroup.substring(token.length(), matchedGroup.length()); + if (DateTimeUtils.containsTime(time)) { + dateTimeMap.put(token, DateTimeUtils.parseNaturalLanguageDateTimeString(time)); + } else { + titleBuilder.append(matchedGroup); + } + }; + executeOnEveryMatcherToken(matcher, consumer); + + String title = titleBuilder.toString(); + + if (title.length() == 0) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + boolean hasDeadlineKeyword = dateTimeMap.containsKey(ARGS_BY); + boolean hasStartTimeKeyword = dateTimeMap.containsKey(ARGS_FROM); + boolean hasEndTimeKeyword = dateTimeMap.containsKey(ARGS_TO); + + if (hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new AddCommand(title, dateTimeMap.get(ARGS_BY)); + } + + if (!hasDeadlineKeyword && hasStartTimeKeyword && hasEndTimeKeyword) { + return new AddCommand(title, dateTimeMap.get(ARGS_FROM), dateTimeMap.get(ARGS_TO)); + } + + if (!hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new AddCommand(title); + } + + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } catch (IllegalValueException ive) { + return new IncorrectCommand(ive.getMessage()); + } + } + + + /** + * Parses arguments in the context of the schedule task command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareSchedule(String args) { + Matcher matcher = ADD_SCHEDULE_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ScheduleCommand.MESSAGE_USAGE)); + } + + matcher.reset(); + matcher.find(); + HashMap> dateTimeMap = new HashMap<>(); + Optional taskIndex = parseIndex(matcher.group(0)); + int index = 0; + if (taskIndex.isPresent()) { + index = taskIndex.get(); + } else { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ScheduleCommand.MESSAGE_USAGE)); + } + + // Run this function on all matched groups + BiConsumer consumer = (matchedGroup, token) -> { + String time = matchedGroup.substring(token.length(), matchedGroup.length()); + if (DateTimeUtils.containsTime(time)) { + dateTimeMap.put(token, DateTimeUtils.parseNaturalLanguageDateTimeString(time)); + } + }; + executeOnEveryMatcherToken(matcher, consumer); + + boolean hasDeadlineKeyword = dateTimeMap.containsKey(ARGS_BY); + boolean hasStartTimeKeyword = dateTimeMap.containsKey(ARGS_FROM); + boolean hasEndTimeKeyword = dateTimeMap.containsKey(ARGS_TO); + + if (hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new ScheduleCommand(index, Optional.empty(), dateTimeMap.get(ARGS_BY)); + } + + if (!hasDeadlineKeyword && hasStartTimeKeyword && hasEndTimeKeyword) { + return new ScheduleCommand(index, dateTimeMap.get(ARGS_FROM), dateTimeMap.get(ARGS_TO));} + + if (!hasDeadlineKeyword && !hasStartTimeKeyword && !hasEndTimeKeyword) { + return new ScheduleCommand(index, Optional.empty(), Optional.empty()); + } + + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE)); + } + + /** + * Checks if there are any quotation marks in the given string + * + * @param str + * @return returns the string inside the quote. + */ + private Optional checkForQuotation(String str) { + Matcher matcher = QUOTATION_FORMAT.matcher(str.trim()); + if (!matcher.find()) { + return Optional.empty(); + } + return Optional.of(matcher.group(0)); + } + + /** + * A higher order function that parses arguments in the context of the schedule task command. + * Extracted out of prepareAdd and prepareSchedule for code reuse. + * + * @param matcher matcher for current command context + * @param consumer closure to execute on + */ + private void executeOnEveryMatcherToken(Matcher matcher, BiConsumer consumer) { + while (matcher.find()) { + for (String token : TIME_TOKENS) { + String matchedGroup = matcher.group(0).toLowerCase(); + if (matchedGroup.startsWith(token)) { + consumer.accept(matchedGroup, token); + } + } + } + } + + + //@@author A0133367E + /** + * Parses arguments in the context of the delete task(s) command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareDelete(String args) { + Set taskIds = parseIndexes(args); + if (taskIds.isEmpty()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + return new DeleteCommand(taskIds); + } + + /** + * Parses arguments in the context of the mark task(s) command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareMark(String args) { + Set taskIds = parseIndexes(args); + if (taskIds.isEmpty()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkCommand.MESSAGE_USAGE)); + } + + return new MarkCommand(taskIds); + } + + /** + * Parses arguments in the context of the unmark task(s) command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareUnmark(String args) { + Set taskIds = parseIndexes(args); + if (taskIds.isEmpty()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnmarkCommand.MESSAGE_USAGE)); + } + + return new UnmarkCommand(taskIds); + } + + /** + * Parses arguments in the context of the rename task command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareRename(String args) { + final Matcher matcher = RENAME_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RenameCommand.MESSAGE_USAGE)); + } + + final String givenName = matcher.group("name").trim(); + final String givenIndex = matcher.group("targetIndex"); + Optional index = parseIndex(givenIndex); + + if (!index.isPresent()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RenameCommand.MESSAGE_USAGE)); + } + + try { + return new RenameCommand(index.get(), givenName); + } catch (IllegalValueException ive) { + return new IncorrectCommand(ive.getMessage()); + } + } + + /** + * Parses arguments in the context of the alias command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareAlias(String args) { + final Matcher matcher = ALIAS_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AliasCommand.MESSAGE_USAGE)); + } + + String aliasKey = matcher.group("shorthand").toLowerCase(); + String aliasValue = matcher.group("commandword").toLowerCase(); + + return new AliasCommand(aliasKey, aliasValue); + } + + /** + * Parses arguments in the context of the unalias command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareUnalias(String args) { + final Matcher matcher = UNALIAS_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnaliasCommand.MESSAGE_USAGE)); + } + + String aliasKey = matcher.group("shorthand").toLowerCase(); + + return new UnaliasCommand(aliasKey); + } + + //@@author + /** + * Returns the specified index in the {@code command} IF a positive unsigned integer is given as the index. + * Returns an {@code Optional.empty()} otherwise. + */ + private Optional parseIndex(String command) { + final Matcher matcher = TASK_INDEX_ARGS_FORMAT.matcher(command.trim()); + if (!matcher.matches()) { + return Optional.empty(); + } + + String index = matcher.group("targetIndex"); + if(!StringUtil.isUnsignedInteger(index)){ + return Optional.empty(); + } + + return Optional.of(Integer.parseInt(index)); + } + + //@@author A0133367E + /** + * Returns the specified indices in the {@code command} if positive unsigned integer(s) are given. + * Returns an empty set otherwise. + */ + private Set parseIndexes(String args) { + final Matcher matcher = TASK_INDEXES_ARGS_FORMAT.matcher(args.trim()); + Set emptySet = new HashSet(); + Set taskIds = new HashSet(); + + if (!matcher.matches()) { + return emptySet; + } + + String replacedArgs = args.replaceAll("[ ]+", ",").replaceAll(",+", ","); + + String[] taskIdStrings = replacedArgs.split(","); + for (String taskIdString : taskIdStrings) { + if (taskIdString.matches("\\d+")) { + taskIds.add(Integer.parseInt(taskIdString)); + } else if (taskIdString.matches("\\d+-\\d+")) { + String[] startAndEndIndexes = taskIdString.split("-"); + int startIndex = Integer.parseInt(startAndEndIndexes[0]); + int endIndex = Integer.parseInt(startAndEndIndexes[1]); + taskIds.addAll(IntStream.rangeClosed(startIndex, endIndex) + .boxed().collect(Collectors.toList())); + } + } + + if (taskIds.remove(0)) { + return emptySet; + } + + return taskIds; + } + + //@@author + + /** + * Parses arguments in the context of the find task command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareFind(String args) { + final Matcher matcher = KEYWORDS_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindCommand.MESSAGE_USAGE)); + } + + // keywords delimited by whitespace + final String[] keywords = matcher.group("keywords").split("\\s+"); + final Set keywordSet = new HashSet<>(Arrays.asList(keywords)); + return new FindCommand(keywordSet); + } + + //@@author A0003878Y + /** + * Parses arugments in the context of the sync command. + * + * @param args full command args string + * @return the prepared command + */ + private Command prepareSync(String args) { + try { + return new SyncCommand(args); + } catch (IllegalValueException ive) { + return new IncorrectCommand(ive.getMessage()); + } + } + + //@@author +} \ No newline at end of file diff --git a/src/main/java/seedu/agendum/model/Model.java b/src/main/java/seedu/agendum/model/Model.java new file mode 100644 index 000000000000..aced4fed5ad9 --- /dev/null +++ b/src/main/java/seedu/agendum/model/Model.java @@ -0,0 +1,75 @@ +package seedu.agendum.model; + +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.commons.events.storage.LoadDataCompleteEvent; +import seedu.agendum.model.ModelManager.NoPreviousListFoundException; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; +import seedu.agendum.model.task.UniqueTaskList; + +import java.util.List; +import java.util.Set; + +/** + * The API of the Model component. + */ +public interface Model { + /** Clears existing backing model and replaces with the provided new data. */ + void resetData(ReadOnlyToDoList newData); + + /** Returns the ToDoList */ + ReadOnlyToDoList getToDoList(); + + /** Deletes the given task(s) */ + void deleteTasks(List targets) throws UniqueTaskList.TaskNotFoundException; + + /** Adds the given task */ + void addTask(Task task) throws UniqueTaskList.DuplicateTaskException; + + /** Updates the given task */ + void updateTask(ReadOnlyTask target, Task updatedTask) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException; + + /** Marks the given task(s) as completed */ + void markTasks(List targets) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException; + + /** Unmarks the given task(s) */ + void unmarkTasks(List targets) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException; + + /** + * Restores the previous to-do list saved in the stack of previous lists. + */ + void restorePreviousToDoList() throws NoPreviousListFoundException; + + /** + * Resets the main to-do list data to match the top list in the stack of previous lists + */ + void resetDataToLastSavedList(); + + /** Returns the filtered task list as an {@code UnmodifiableObservableList} */ + UnmodifiableObservableList getFilteredTaskList(); + + /** Updates the filter of the filtered task list to show all tasks */ + void updateFilteredListToShowAll(); + + /** Updates the filter of the filtered task list to filter by the given keywords*/ + void updateFilteredTaskList(Set keywords); + + /** Change the data storage location */ + void changeSaveLocation(String location); + + /** load the data from a file **/ + void loadFromLocation(String location); + + /** Updates the current todolist to the loaded data**/ + void handleLoadDataCompleteEvent(LoadDataCompleteEvent event); + + /** Turns on model syncing using to a thirds party syncing provider **/ + void activateModelSyncing(); + + /** Turns off model syncing **/ + void deactivateModelSyncing(); + +} diff --git a/src/main/java/seedu/agendum/model/ModelManager.java b/src/main/java/seedu/agendum/model/ModelManager.java new file mode 100644 index 000000000000..7d7cc0fba213 --- /dev/null +++ b/src/main/java/seedu/agendum/model/ModelManager.java @@ -0,0 +1,359 @@ +package seedu.agendum.model; + +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.commons.util.XmlUtil; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; +import seedu.agendum.model.task.UniqueTaskList; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; +import seedu.agendum.commons.events.model.LoadDataRequestEvent; +import seedu.agendum.commons.events.model.ChangeSaveLocationEvent; +import seedu.agendum.commons.events.model.ToDoListChangedEvent; +import seedu.agendum.commons.events.storage.LoadDataCompleteEvent; +import seedu.agendum.commons.core.ComponentManager; + +import java.util.List; +import java.util.Set; +import java.util.Stack; +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; +import seedu.agendum.sync.Sync; +import seedu.agendum.sync.SyncManager; +import seedu.agendum.sync.SyncProviderGoogle; + +/** + * Represents the in-memory model of the to do list data. + * All changes to any model should be synchronized. + */ +public class ModelManager extends ComponentManager implements Model { + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + + private final ToDoList mainToDoList; + private final Stack previousLists; + private final FilteredList filteredTasks; + private final SortedList sortedTasks; + + private final SyncManager syncManager; + + //@@author A0133367E + /** + * Signals that an operation to remove a list from the stack of previous lists would fail + * as the stack must contain at least one list. + */ + public static class NoPreviousListFoundException extends Exception {} + //@@author + + /** + * Initializes a ModelManager with the given ToDoList + * ToDoList and its variables should not be null + */ + public ModelManager(ToDoList src, UserPrefs userPrefs) { + super(); + assert src != null; + assert userPrefs != null; + + logger.fine("Initializing with to do list: " + src + " and user prefs " + userPrefs); + + mainToDoList = new ToDoList(src); + filteredTasks = new FilteredList<>(mainToDoList.getTasks()); + sortedTasks = filteredTasks.sorted(); + previousLists = new Stack(); + backupCurrentToDoList(); + + syncManager = new SyncManager(new SyncProviderGoogle()); + } + + public ModelManager() { + this(new ToDoList(), new UserPrefs()); + } + + public ModelManager(ReadOnlyToDoList initialData) { + mainToDoList = new ToDoList(initialData); + filteredTasks = new FilteredList(mainToDoList.getTasks()); + sortedTasks = filteredTasks.sorted(); + previousLists = new Stack(); + backupCurrentToDoList(); + + syncManager = new SyncManager(new SyncProviderGoogle()); + } + + //@@author A0133367E + @Override + public void resetData(ReadOnlyToDoList newData) { + mainToDoList.resetData(newData); + logger.fine("[MODEL] --- successfully reset data of the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } + //@@author + + @Override + public ReadOnlyToDoList getToDoList() { + return mainToDoList; + } + + /** Raises an event to indicate the model has changed */ + private void indicateToDoListChanged() { + // force a reset/refresh for list view in UI + mainToDoList.resetData(mainToDoList); + raise(new ToDoListChangedEvent(mainToDoList)); + } + + //@@author A0148095X + /** Raises an event to indicate that save location has changed */ + private void indicateChangeSaveLocation(String location) { + raise(new ChangeSaveLocationEvent(location)); + } + + /** Raises an event to indicate that save location has changed */ + private void indicateLoadDataRequest(String location) { + raise(new LoadDataRequestEvent(location)); + } + + //@@author A0133367E + @Override + public synchronized void deleteTasks(List targets) throws TaskNotFoundException { + for (ReadOnlyTask target: targets) { + mainToDoList.removeTask(target); + removeTaskFromSyncManager(target); + } + + logger.fine("[MODEL] --- successfully deleted all specified targets from the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } + + @Override + public synchronized void addTask(Task task) throws UniqueTaskList.DuplicateTaskException { + mainToDoList.addTask(task); + + logger.fine("[MODEL] --- successfully added the new task to the to-do list"); + backupCurrentToDoList(); + updateFilteredListToShowAll(); + indicateToDoListChanged(); + addTaskToSyncManager(task); + } + + @Override + public synchronized void updateTask(ReadOnlyTask target, Task updatedTask) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + mainToDoList.updateTask(target, updatedTask); + + logger.fine("[MODEL] --- successfully updated the target task in the to-do list"); + backupCurrentToDoList(); + updateFilteredListToShowAll(); + indicateToDoListChanged(); + + addTaskToSyncManager(updatedTask); + removeTaskFromSyncManager(target); + } + + @Override + public synchronized void markTasks(List targets) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + for (ReadOnlyTask target: targets) { + mainToDoList.markTask(target); + } + + logger.fine("[MODEL] --- successfully marked all specified targets from the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } + + @Override + public synchronized void unmarkTasks(List targets) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + for (ReadOnlyTask target: targets) { + mainToDoList.unmarkTask(target); + } + + logger.fine("[MODEL] --- successfully unmarked all specified targets from the to-do list"); + backupCurrentToDoList(); + indicateToDoListChanged(); + } + + /** + * Restores the previous (second latest) list saved in the stack of previous lists. + */ + @Override + public synchronized void restorePreviousToDoList() throws NoPreviousListFoundException { + removeLastSavedToDoList(); + resetDataToLastSavedList(); + logger.fine("[MODEL] --- successfully restored the previous the to-do list from this session"); + indicateToDoListChanged(); + } + + /** + * Resets the {@code mainToDoList} data to match the top list in the {@code previousLists} stack + */ + @Override + public void resetDataToLastSavedList() { + assert !previousLists.empty(); + ToDoList lastSavedListFromHistory = previousLists.peek(); + mainToDoList.resetData(lastSavedListFromHistory); + } + + private void backupCurrentToDoList() { + ToDoList latestList = new ToDoList(this.getToDoList()); + previousLists.push(latestList); + } + + private void clearAllPreviousToDoLists() { + previousLists.clear(); + } + + /** + * Pops the top list from the {@code previousLists} stack if there are more than 1 list present + * @throws NoPreviousListFoundException if there is only 1 list in the stack + */ + private void removeLastSavedToDoList() throws NoPreviousListFoundException { + assert !previousLists.empty(); + + if (previousLists.size() == 1) { + throw new NoPreviousListFoundException(); + } + + previousLists.pop(); + } + + + //@@author A0148095X + //=========== Storage Methods ========================================================================== + + @Override + public synchronized void changeSaveLocation(String location){ + assert StringUtil.isValidPathToFile(location); + indicateChangeSaveLocation(location); + indicateToDoListChanged(); + } + + @Override + public synchronized void loadFromLocation(String location) { + assert StringUtil.isValidPathToFile(location); + assert XmlUtil.isFileCorrectFormat(location); + + indicateChangeSaveLocation(location); + indicateLoadDataRequest(location); + } + + private void addTaskToSyncManager(Task task) { + syncManager.addNewEvent(task); + } + + private void removeTaskFromSyncManager(ReadOnlyTask task) { + syncManager.deleteEvent((Task) task); + } + //@@author A0003878Y + + //=========== Sync Methods =============================================================================== + + @Override + public void activateModelSyncing() { + if (syncManager.getSyncStatus() != Sync.SyncStatus.RUNNING) { + syncManager.startSyncing(); + + // Add all current events into sync provider + mainToDoList.getTasks().forEach(syncManager::addNewEvent); + } + } + + @Override + public void deactivateModelSyncing() { + if (syncManager.getSyncStatus() != Sync.SyncStatus.NOTRUNNING) { + syncManager.stopSyncing(); + } + } + + + + //@@author + //=========== Filtered Task List Accessors =============================================================== + + @Override + public UnmodifiableObservableList getFilteredTaskList() { + return new UnmodifiableObservableList<>(sortedTasks); + } + + @Override + public void updateFilteredListToShowAll() { + filteredTasks.setPredicate(null); + } + + @Override + public void updateFilteredTaskList(Set keywords){ + updateFilteredTaskList(new PredicateExpression(new NameQualifier(keywords))); + } + + private void updateFilteredTaskList(Expression expression) { + filteredTasks.setPredicate(expression::satisfies); + } + + //========== Inner classes/interfaces used for filtering ================================================== + + interface Expression { + boolean satisfies(ReadOnlyTask task); + String toString(); + } + + private class PredicateExpression implements Expression { + + private final Qualifier qualifier; + + PredicateExpression(Qualifier qualifier) { + this.qualifier = qualifier; + } + + @Override + public boolean satisfies(ReadOnlyTask task) { + return qualifier.run(task); + } + + @Override + public String toString() { + return qualifier.toString(); + } + } + + interface Qualifier { + boolean run(ReadOnlyTask task); + String toString(); + } + + private class NameQualifier implements Qualifier { + private Set nameKeyWords; + + NameQualifier(Set nameKeyWords) { + this.nameKeyWords = nameKeyWords; + } + + @Override + public boolean run(ReadOnlyTask task) { + return nameKeyWords.stream() + .filter(keyword -> StringUtil.containsIgnoreCase(task.getName().fullName, keyword)) + .findAny() + .isPresent(); + } + + @Override + public String toString() { + return "name=" + String.join(", ", nameKeyWords); + } + } + + //========== event handling ================================================== + //@@author A0148095X + @Override + @Subscribe + public void handleLoadDataCompleteEvent(LoadDataCompleteEvent event) { + this.mainToDoList.resetData(event.data); + indicateToDoListChanged(); + clearAllPreviousToDoLists(); + backupCurrentToDoList(); + logger.info("Loading completed - Todolist updated."); + } +} diff --git a/src/main/java/seedu/agendum/model/ReadOnlyToDoList.java b/src/main/java/seedu/agendum/model/ReadOnlyToDoList.java new file mode 100644 index 000000000000..25f8e1d86797 --- /dev/null +++ b/src/main/java/seedu/agendum/model/ReadOnlyToDoList.java @@ -0,0 +1,21 @@ +package seedu.agendum.model; + + +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.UniqueTaskList; + +import java.util.List; + +/** + * Unmodifiable view of a to do list + */ +public interface ReadOnlyToDoList { + + UniqueTaskList getUniqueTaskList(); + + /** + * Returns an unmodifiable view of tasks list + */ + List getTaskList(); + +} diff --git a/src/main/java/seedu/agendum/model/ToDoList.java b/src/main/java/seedu/agendum/model/ToDoList.java new file mode 100644 index 000000000000..6a66a2253896 --- /dev/null +++ b/src/main/java/seedu/agendum/model/ToDoList.java @@ -0,0 +1,144 @@ +package seedu.agendum.model; + +import javafx.collections.ObservableList; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; +import seedu.agendum.model.task.UniqueTaskList; +import seedu.agendum.model.task.UniqueTaskList.DuplicateTaskException; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Wraps all data at the to do list level + * Duplicates are not allowed (by .equals comparison) + */ +public class ToDoList implements ReadOnlyToDoList { + + private final UniqueTaskList tasks; + + { + tasks = new UniqueTaskList(); + } + + public ToDoList() {} + + /** + * Tasks are copied into this to do list + */ + public ToDoList(ReadOnlyToDoList toBeCopied) { + this(toBeCopied.getUniqueTaskList()); + } + + /** + * Tasks are copied into this to do list + */ + public ToDoList(UniqueTaskList tasks) { + resetData(tasks.getInternalList()); + } + + public static ReadOnlyToDoList getEmptyToDoList() { + return new ToDoList(); + } + +//// list overwrite operations + + public ObservableList getTasks() { + return tasks.getInternalList(); + } + + public void setTasks(List tasks) { + this.tasks.getInternalList().setAll(tasks); + } + + public void resetData(Collection newTasks) { + setTasks(newTasks.stream().map(Task::new).collect(Collectors.toList())); + } + + public void resetData(ReadOnlyToDoList newData) { + resetData(newData.getTaskList()); + } + +//// task-level operations + + /** + * Adds a task to the to-do list. + * + * @throws DuplicateTaskException if an equivalent task already exists. + */ + public void addTask(Task p) throws DuplicateTaskException { + tasks.add(p); + } + + public boolean removeTask(ReadOnlyTask key) throws TaskNotFoundException { + return tasks.remove(key); + } + + //@@author A0133367E + /** + * Updates an existing task in the to-do list. + * + * @throws DuplicateTaskException if an equivalent task (to updatedTask) already exists. + * @throws TaskNotFoundException if no such task (key) could be found in the list. + */ + public boolean updateTask(ReadOnlyTask key, Task updatedTask) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + return tasks.update(key, updatedTask); + } + + /** + * Marks an existing task in the to-do list. + * + * @throws DuplicateTaskException if a duplicate task would result after marking key. + * @throws TaskNotFoundException if no such task (key) could be found in the list. + */ + public boolean markTask(ReadOnlyTask key) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + return tasks.mark(key); + } + + /** + * Unmarks an existing task in the to-do list. + * + * @throws DuplicateTaskException if a duplicate task would result after unmarking key. + * @throws TaskNotFoundException if no such task (key) could be found in the list. + */ + public boolean unmarkTask(ReadOnlyTask key) + throws UniqueTaskList.TaskNotFoundException, UniqueTaskList.DuplicateTaskException { + return tasks.unmark(key); + } + //@@author + +//// util methods + + @Override + public String toString() { + return tasks.getInternalList().size() + " tasks, " ; + // TODO: refine later + } + + @Override + public List getTaskList() { + return Collections.unmodifiableList(tasks.getInternalList()); + } + + @Override + public UniqueTaskList getUniqueTaskList() { + return this.tasks; + } + + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ToDoList // instanceof handles nulls + && this.tasks.equals(((ToDoList) other).tasks)); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(tasks); + } +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/agendum/model/UserPrefs.java similarity index 73% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/seedu/agendum/model/UserPrefs.java index da9c8037f495..c74371707c0f 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/agendum/model/UserPrefs.java @@ -1,7 +1,8 @@ -package seedu.address.model; - -import seedu.address.commons.core.GuiSettings; +package seedu.agendum.model; +import seedu.agendum.commons.core.GuiSettings; +import java.awt.Dimension; +import java.awt.Toolkit; import java.util.Objects; /** @@ -19,14 +20,20 @@ public void updateLastUsedGuiSetting(GuiSettings guiSettings) { this.guiSettings = guiSettings; } + //@@author A0148031R + /** + * Sets default window to be screen size + */ public UserPrefs(){ - this.setGuiSettings(500, 500, 0, 0); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + this.setGuiSettings(screenSize.getWidth(), screenSize.getHeight(), 0, 0); } public void setGuiSettings(double width, double height, int x, int y) { guiSettings = new GuiSettings(width, height, x, y); } + //@@author @Override public boolean equals(Object other) { if (other == this){ diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/agendum/model/task/Name.java similarity index 67% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/agendum/model/task/Name.java index 4f30033e70fe..c4364ef0b6df 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/agendum/model/task/Name.java @@ -1,15 +1,15 @@ -package seedu.address.model.person; +package seedu.agendum.model.task; -import seedu.address.commons.exceptions.IllegalValueException; +import seedu.agendum.commons.exceptions.IllegalValueException; /** - * Represents a Person's name in the address book. + * Represents a Task's name in the to do list. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ public class Name { - public static final String MESSAGE_NAME_CONSTRAINTS = "Person names should be spaces or alphanumeric characters"; - public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum} ]+"; + public static final String MESSAGE_NAME_CONSTRAINTS = "All task names are valid"; + public static final String NAME_VALIDATION_REGEX = ".+"; public final String fullName; @@ -20,15 +20,15 @@ public class Name { */ public Name(String name) throws IllegalValueException { assert name != null; - name = name.trim(); - if (!isValidName(name)) { + String trimmedName = name.trim(); + if (!isValidName(trimmedName)) { throw new IllegalValueException(MESSAGE_NAME_CONSTRAINTS); } - this.fullName = name; + this.fullName = trimmedName; } /** - * Returns true if a given string is a valid person name. + * Returns true if a given string is a valid task name. */ public static boolean isValidName(String test) { return test.matches(NAME_VALIDATION_REGEX); diff --git a/src/main/java/seedu/agendum/model/task/ReadOnlyTask.java b/src/main/java/seedu/agendum/model/task/ReadOnlyTask.java new file mode 100644 index 000000000000..9af3b7de8c05 --- /dev/null +++ b/src/main/java/seedu/agendum/model/task/ReadOnlyTask.java @@ -0,0 +1,69 @@ +package seedu.agendum.model.task; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * A read-only immutable interface for a Task in the ToDoList. + * Implementations should guarantee: details are present and not null, field values are validated. + */ +public interface ReadOnlyTask { + + Name getName(); + boolean isCompleted(); + boolean isUpcoming(); + boolean isOverdue(); + boolean isEvent(); + boolean hasDeadline(); + boolean hasTime(); + Optional getStartDateTime(); + Optional getEndDateTime(); + LocalDateTime getLastUpdatedTime(); + + /** + * Returns true if both have the same state. (interfaces cannot override .equals) + */ + default boolean isSameStateAs(ReadOnlyTask other) { + return other == this // short circuit if same object + || (other != null // this is first to avoid NPE below + && other.getName().equals(this.getName()) // state checks here onwards + && (other.isCompleted() == this.isCompleted()) + && other.getStartDateTime().equals(this.getStartDateTime()) + && other.getEndDateTime().equals(this.getEndDateTime())); + } + + /** + * Formats the task as text, showing task name only. + */ + default String getAsText() { + return String.valueOf(getName()) + "\n"; + } + + //@@author A0133367E + /** + * Format the tasks as text, showing all fine details including name, + * completion status, start and end time if any and last updated time + */ + default String getDetailedText() { + String completionStatus = (isCompleted()) ? "Completed" : "Incomplete"; + String startTime = (getStartDateTime().isPresent()) ? getStartDateTime().get().toString() + : "None"; + String endTime = (getEndDateTime().isPresent()) ? getEndDateTime().get().toString() + : "None"; + String lastUpdatedTime = getLastUpdatedTime().toString(); + + final StringBuilder builder = new StringBuilder(); + builder.append("Task name: ") + .append(getName()) + .append(" Completion Status: ") + .append(completionStatus) + .append(" Start Time: ") + .append(startTime) + .append(" End Time: ") + .append(endTime) + .append(" Last Updated Time: ") + .append(lastUpdatedTime); + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/agendum/model/task/Task.java b/src/main/java/seedu/agendum/model/task/Task.java new file mode 100644 index 000000000000..1e6303559089 --- /dev/null +++ b/src/main/java/seedu/agendum/model/task/Task.java @@ -0,0 +1,316 @@ +package seedu.agendum.model.task; + +import seedu.agendum.commons.util.CollectionUtil; + +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; + +//@@author A0133367E +/** + * Represents a Task in the to do list. + */ +public class Task implements ReadOnlyTask, Comparable { + + private static final int UPCOMING_DAYS_THRESHOLD = 7; + + private Name name_; + private boolean isCompleted_; + private LocalDateTime startDateTime_; + private LocalDateTime endDateTime_; + private LocalDateTime lastUpdatedTime_; + + // ================ Constructor methods ============================== + + /** + * Constructor for a floating task (with no deadline/start time or end time) + */ + public Task(Name name) { + assert CollectionUtil.isNotNull(name); + this.name_ = name; + this.isCompleted_ = false; + this.startDateTime_ = null; + this.endDateTime_ = null; + setLastUpdatedTimeToNow(); + } + + /** + * Constructor for a task with deadline only + */ + public Task(Name name, Optional deadline) { + assert CollectionUtil.isNotNull(name); + this.name_ = name; + this.isCompleted_ = false; + this.startDateTime_ = null; + this.endDateTime_ = deadline.orElse(null); + this.setLastUpdatedTimeToNow(); + } + + /** + * Constructor for a task (event) with both a start and end time + */ + public Task(Name name, Optional startDateTime, + Optional endDateTime) { + assert CollectionUtil.isNotNull(name); + this.name_ = name; + this.isCompleted_ = false; + this.startDateTime_ = startDateTime.orElse(null); + this.endDateTime_ = endDateTime.orElse(null); + this.setLastUpdatedTimeToNow(); + } + + /** + * Copy constructor. + */ + public Task(ReadOnlyTask source) { + this(source.getName(), source.getStartDateTime(), source.getEndDateTime()); + if (source.isCompleted()) { + this.markAsCompleted(); + } + this.setLastUpdatedTime(source.getLastUpdatedTime()); + } + + // ================ Getter methods ============================== + + @Override + public Name getName() { + return name_; + } + + @Override + public boolean isCompleted() { + return isCompleted_; + } + + /** + * Returns true if a task is uncompleted and has a start/end time + * that is after the current time but within some threshold amount of days + */ + @Override + public boolean isUpcoming() { + if (isCompleted()) { + return false; + } + + if (!hasTime()) { + return false; + } + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime thresholdTime = currentTime.plusDays(UPCOMING_DAYS_THRESHOLD); + boolean isBeforeUpcomingDaysThreshold = getTaskTime().isBefore(thresholdTime); + boolean isAfterCurrentTime = getTaskTime().isAfter(currentTime); + + return isBeforeUpcomingDaysThreshold && isAfterCurrentTime; + } + + /** + * Returns true is a task is uncompleted and has a start/end time + * that is before the current time + */ + @Override + public boolean isOverdue() { + if (isCompleted()) { + return false; + } + + if (!hasTime()) { + return false; + } + + LocalDateTime currentTime = LocalDateTime.now(); + boolean isBeforeCurrentTime = getTaskTime().isBefore(currentTime); + + return isBeforeCurrentTime; + } + + /** + * Returns true if a task has a start time or an end time, false otherwise + * This must be called to check if comparison of task's time is possible + */ + @Override + public boolean hasTime() { + return getStartDateTime().isPresent() || getEndDateTime().isPresent(); + } + + /** + * Returns true if the task has a start time and end time, false otherwise. + */ + @Override + public boolean isEvent() { + return getStartDateTime().isPresent() && getEndDateTime().isPresent(); + } + + /** + * Returns true if the task has a deadline (i.e. only a end time), false otherwise. + */ + @Override + public boolean hasDeadline() { + return !getStartDateTime().isPresent() && getEndDateTime().isPresent(); + } + + @Override + public Optional getStartDateTime() { + return Optional.ofNullable(startDateTime_); + } + + @Override + public Optional getEndDateTime() { + return Optional.ofNullable(endDateTime_); + } + + /** + * Returns the time the task is last updated. + * e.g. created, renamed, rescheduled, marked or unmarked + */ + @Override + public LocalDateTime getLastUpdatedTime() { + return lastUpdatedTime_; + } + + /** + * Pre-condition: Task has a start or end time. + * Returns the start time if present, else returns the end time. + */ + private LocalDateTime getTaskTime() { + assert hasTime(); + return getStartDateTime().orElse(getEndDateTime().get()); + } + + // ================ Setter methods ============================== + + public void setName(Name name) { + this.name_ = name; + setLastUpdatedTimeToNow(); + } + + public void markAsCompleted() { + this.isCompleted_ = true; + setLastUpdatedTimeToNow(); + } + + public void markAsUncompleted() { + this.isCompleted_ = false; + setLastUpdatedTimeToNow(); + } + + public void setStartDateTime(Optional startDateTime) { + this.startDateTime_ = startDateTime.orElse(null); + setLastUpdatedTimeToNow(); + } + + public void setEndDateTime(Optional endDateTime) { + this.endDateTime_ = endDateTime.orElse(null); + setLastUpdatedTimeToNow(); + } + + public void setLastUpdatedTime(LocalDateTime updatedTime) { + this.lastUpdatedTime_ = updatedTime; + } + + public void setLastUpdatedTimeToNow() { + // nano-seconds is set to 0 for more consistent test results when (un)marking multiple tasks + this.lastUpdatedTime_ = LocalDateTime.now().withNano(0); + } + + // ================ Other methods ============================== + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ReadOnlyTask // instanceof handles nulls + && this.isSameStateAs((ReadOnlyTask) other)); + } + + /** + * Compares the current task with another Task other. + * The current task is considered to be less than the other task if + * 1) it is uncompleted and other is completed + * 2) both tasks are completed but this task has a earlier start/end time associated + * 3) both tasks are uncompleted but this task has a later updated time + * 4) both tasks are uncompleted with the same updated time + * but this task has a lexicographically smaller name (useful when sorting tasks in testing) + */ + @Override + public int compareTo(Task other) { + int comparedCompletionStatus = compareCompletionStatus(other); + if (comparedCompletionStatus != 0) { + return comparedCompletionStatus; + } + + int comparedTaskTime = compareTaskTime(other); + if (!isCompleted() && comparedTaskTime != 0) { + return comparedTaskTime; + } + + int comparedLastUpdatedTime = compareLastUpdatedTime(other); + if (comparedLastUpdatedTime != 0) { + return comparedLastUpdatedTime; + } + + return compareName(other); + } + + /** + * Compares the completion status of current task with another Task other. + * The current task is considered to be less than the other task if + * it is uncompleted and other is completed + */ + public int compareCompletionStatus(Task other) { + return Boolean.compare(this.isCompleted(), other.isCompleted()); + } + + /** + * Compares the earliest time of the current task with another Task other. + * The current task is considered to be less than the other task if + * 1) both tasks have a time associated but this task has a earlier time associated + * 2) this task has a time associated but the other task does not. + * Both tasks are equal if they have no time or the same earliest time associated. + * Time refers to value returned by {@link #getTaskTime()} + */ + public int compareTaskTime(Task other) { + if (this.hasTime() && other.hasTime()) { + return this.getTaskTime().compareTo(other.getTaskTime()); + } else if (this.hasTime()) { + return -1; + } else if (other.hasTime()) { + return 1; + } else { + return 0; + } + } + + /** + * Compares the current task with another Task other. + * The current task is considered to be less than the other task if + * it has a later updated time + */ + public int compareLastUpdatedTime(Task other) { + return other.getLastUpdatedTime().compareTo(this.getLastUpdatedTime()); + } + + /** + * Compares the current task with another Task other. + * The current task is considered to be less than the other task if + * it has a lexicographically smaller name + */ + public int compareName(Task other) { + return this.getName().toString().compareTo(other.getName().toString()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name_, isCompleted_, startDateTime_, endDateTime_); + } + + public int syncCode() { + return hashCode(); + } + + @Override + public String toString() { + return getAsText(); + } + +} diff --git a/src/main/java/seedu/agendum/model/task/UniqueTaskList.java b/src/main/java/seedu/agendum/model/task/UniqueTaskList.java new file mode 100644 index 000000000000..8c08a2f07b27 --- /dev/null +++ b/src/main/java/seedu/agendum/model/task/UniqueTaskList.java @@ -0,0 +1,185 @@ +package seedu.agendum.model.task; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.agendum.commons.util.CollectionUtil; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.events.ui.JumpToListRequestEvent; +import seedu.agendum.commons.exceptions.DuplicateDataException; + +import java.util.*; +import java.util.logging.Logger; + +/** + * A list of tasks that enforces uniqueness between its elements and does not allow nulls. + * + * Supports a minimal set of list operations. + * + * @see Task#equals(Object) + * @see CollectionUtil#elementsAreUnique(Collection) + */ +public class UniqueTaskList implements Iterable { + private static final Logger logger = LogsCenter.getLogger(UniqueTaskList.class); + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Signals that an operation would have violated the 'no duplicates' property of the list. + */ + public static class DuplicateTaskException extends DuplicateDataException { + protected DuplicateTaskException() { + super("Operation would result in duplicate tasks"); + } + } + + /** + * Signals that an operation targeting a specified task in the list would fail because + * there is no such matching task in the list. + */ + public static class TaskNotFoundException extends Exception {} + + /** + * Constructs empty TaskList. + */ + public UniqueTaskList() {} + + /** + * Returns true if the list contains an equivalent task as the given argument. + */ + public boolean contains(ReadOnlyTask toCheck) { + assert toCheck != null; + return internalList.contains(toCheck); + } + + //@@author A0133367E + /** + * Adds a task to the list. + * + * @throws DuplicateTaskException if the task to add is a duplicate of an existing task in the list. + */ + public void add(Task toAdd) throws DuplicateTaskException { + assert toAdd != null; + + if (contains(toAdd)) { + logger.fine("[TASK LIST] --- Duplicate Task: " + toAdd.getDetailedText()); + EventsCenter.getInstance().post(new JumpToListRequestEvent(toAdd, false)); + throw new DuplicateTaskException(); + } + + internalList.add(toAdd); + EventsCenter.getInstance().post(new JumpToListRequestEvent(toAdd, false)); + + logger.fine("[TASK LIST] --- Added a Task: " + toAdd.getDetailedText()); + } + + /** + * Removes the equivalent task from the list. + * + * @throws TaskNotFoundException if no such task could be found in the list. + */ + public boolean remove(ReadOnlyTask toRemove) throws TaskNotFoundException { + assert toRemove != null; + final boolean taskFoundAndDeleted = internalList.remove(toRemove); + + if (!taskFoundAndDeleted) { + logger.fine("[TASK LIST] --- Missing Task: " + toRemove.getDetailedText()); + throw new TaskNotFoundException(); + } + + logger.fine("[TASK LIST] --- Deleted a Task: " + toRemove.getDetailedText()); + + return taskFoundAndDeleted; + } + + /** + * Replaces the equivalent task (to toUpdate) in the list with a new task (updatedTask). + * + * @throws TaskNotFoundException if no such task (toUpdate) could be found in the list. + * @throws DuplicateTaskException if the updated task is a duplicate of an existing task in the list. + */ + public boolean update(ReadOnlyTask toUpdate, Task updatedTask) + throws TaskNotFoundException, DuplicateTaskException { + assert toUpdate != null; + assert updatedTask != null; + + final int taskIndex = internalList.indexOf(toUpdate); + final boolean taskFoundAndUpdated = (taskIndex != -1); + + if (!taskFoundAndUpdated) { + logger.fine("[TASK LIST] --- Missing Task: " + toUpdate.getDetailedText()); + throw new TaskNotFoundException(); + } + + if (contains(updatedTask)) { + logger.fine("[TASK LIST] --- Duplicate Task: " + toUpdate.getDetailedText()); + EventsCenter.getInstance().post(new JumpToListRequestEvent(updatedTask, true)); + throw new DuplicateTaskException(); + } + + internalList.set(taskIndex, updatedTask); + EventsCenter.getInstance().post(new JumpToListRequestEvent(updatedTask, true)); + logger.fine("[TASK LIST] --- Updated Task: " + toUpdate.getDetailedText() + + " updated to " + updatedTask.getDetailedText()); + + return taskFoundAndUpdated; + } + + /** + * Marks the equivalent task in the list. + * + * @throws TaskNotFoundException if no such task could be found in the list. + * @throws DuplicateTaskException if a duplicate will result from marking the task + */ + public boolean mark(ReadOnlyTask toMark) throws TaskNotFoundException, DuplicateTaskException { + assert toMark != null; + + logger.fine("[TASK LIST] --- Attempt to Mark Task: " + toMark.getDetailedText()); + + Task markedTask = new Task(toMark); + markedTask.markAsCompleted(); + boolean taskFoundAndMarked = update(toMark, markedTask); + + return taskFoundAndMarked; + } + + /** + * Unmarks the equivalent task in the list. + * + * @throws TaskNotFoundException if no such task could be found in the list. + * @throws DuplicateTaskException if a duplicate will result from unmarking the task + */ + public boolean unmark(ReadOnlyTask toUnmark) throws TaskNotFoundException, DuplicateTaskException { + assert toUnmark != null; + + logger.fine("[TASK LIST] --- Attempt to Unmark Task: " + toUnmark.getDetailedText()); + + Task unmarkedTask = new Task(toUnmark); + unmarkedTask.markAsUncompleted(); + boolean taskFoundAndUnmarked = update(toUnmark, unmarkedTask); + + return taskFoundAndUnmarked; + } + + //@@author + public ObservableList getInternalList() { + return internalList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueTaskList // instanceof handles nulls + && this.internalList.equals( + ((UniqueTaskList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } +} diff --git a/src/main/java/seedu/agendum/storage/AliasTableStorage.java b/src/main/java/seedu/agendum/storage/AliasTableStorage.java new file mode 100644 index 000000000000..c9c7aff92dd4 --- /dev/null +++ b/src/main/java/seedu/agendum/storage/AliasTableStorage.java @@ -0,0 +1,30 @@ +package seedu.agendum.storage; + +import seedu.agendum.commons.exceptions.DataConversionException; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.Optional; + +/** + * Represents a storage for {@link seedu.agendum.logic.commands.CommandLibrary} alias table. + */ +public interface AliasTableStorage { + + /** + * Returns the alias table (map of alias key to reserved command word) from storage. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional> readAliasTable() + throws DataConversionException, IOException; + + /** + * Saves the alias table from {@link seedu.agendum.logic.commands.CommandLibrary} to the storage. + * @param table cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveAliasTable(Hashtable table) throws IOException; + +} diff --git a/src/main/java/seedu/agendum/storage/JsonAliasTableStorage.java b/src/main/java/seedu/agendum/storage/JsonAliasTableStorage.java new file mode 100644 index 000000000000..6a4d424e84af --- /dev/null +++ b/src/main/java/seedu/agendum/storage/JsonAliasTableStorage.java @@ -0,0 +1,75 @@ +package seedu.agendum.storage; + +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.FileUtil; + +import java.io.File; +import java.io.IOException; +import java.util.Hashtable; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * A class to access the alias (command) table stored in the hard disk as a json file + */ +public class JsonAliasTableStorage implements AliasTableStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonAliasTableStorage.class); + + private String filePath; + + public JsonAliasTableStorage(String filePath) { + this.filePath = filePath; + } + + @Override + public Optional> readAliasTable() throws DataConversionException, IOException { + return readAliasTable(filePath); + } + + @Override + public void saveAliasTable(Hashtable table) throws IOException { + saveAliasTable(table, filePath); + } + + /** + * Similar to {@link #readAliasTable()} + * @param aliasTableFilePath location of the command library's alias table data. Cannot be null. + * @throws DataConversionException if the file format is not as expected. + */ + public Optional> readAliasTable(String aliasTableFilePath) + throws DataConversionException { + assert aliasTableFilePath != null; + + File aliasTableFile = new File(aliasTableFilePath); + + if (!aliasTableFile.exists()) { + logger.info("Alias Table file: " + aliasTableFile + " not found"); + return Optional.empty(); + } + + Hashtable table = new Hashtable(); + + try { + table = FileUtil.deserializeObjectFromJsonFile(aliasTableFile, table.getClass()); + } catch (IOException e) { + logger.warning("Error reading from alias table file " + aliasTableFile + ": " + e); + throw new DataConversionException(e); + } + + return Optional.of(table); + } + + /** + * Similar to {@link #saveAliasTable(Hashtable)} + * @param aliasTableFilePath location of the command library's alias table data. Cannot be null. + */ + public void saveAliasTable(Hashtable table, String aliasTableFilePath) + throws IOException { + assert table != null; + assert aliasTableFilePath != null; + + FileUtil.serializeObjectToJsonFile(new File(aliasTableFilePath), table); + } +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/seedu/agendum/storage/JsonUserPrefsStorage.java similarity index 86% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/seedu/agendum/storage/JsonUserPrefsStorage.java index 1efa8288e4f6..6ba52ea785e3 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/seedu/agendum/storage/JsonUserPrefsStorage.java @@ -1,9 +1,9 @@ -package seedu.address.storage; +package seedu.agendum.storage; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.FileUtil; -import seedu.address.model.UserPrefs; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.model.UserPrefs; import java.io.File; import java.io.IOException; @@ -68,6 +68,9 @@ public void saveUserPrefs(UserPrefs userPrefs, String prefsFilePath) throws IOEx assert userPrefs != null; assert prefsFilePath != null; + File file = new File(prefsFilePath); + + FileUtil.createIfMissing(file); FileUtil.serializeObjectToJsonFile(new File(prefsFilePath), userPrefs); } } diff --git a/src/main/java/seedu/agendum/storage/Storage.java b/src/main/java/seedu/agendum/storage/Storage.java new file mode 100644 index 000000000000..48c681025c5f --- /dev/null +++ b/src/main/java/seedu/agendum/storage/Storage.java @@ -0,0 +1,65 @@ +package seedu.agendum.storage; + +import seedu.agendum.commons.events.model.LoadDataRequestEvent; +import seedu.agendum.commons.events.logic.AliasTableChangedEvent; +import seedu.agendum.commons.events.model.ChangeSaveLocationEvent; +import seedu.agendum.commons.events.model.ToDoListChangedEvent; +import seedu.agendum.commons.events.storage.DataSavingExceptionEvent; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.model.ReadOnlyToDoList; +import seedu.agendum.model.UserPrefs; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.Optional; + +/** + * API of the Storage component + */ +public interface Storage extends ToDoListStorage, UserPrefsStorage, AliasTableStorage { + + @Override + Optional> readAliasTable() + throws DataConversionException, IOException; + + @Override + void saveAliasTable(Hashtable table) throws IOException; + + @Override + Optional readUserPrefs() throws DataConversionException, IOException; + + @Override + void saveUserPrefs(UserPrefs userPrefs) throws IOException; + + @Override + String getToDoListFilePath(); + + @Override + void setToDoListFilePath(String filePath); + + @Override + Optional readToDoList() throws DataConversionException, IOException; + + @Override + void saveToDoList(ReadOnlyToDoList toDoList) throws IOException; + + /** + * Saves the current version of the To Do List to the hard disk. + * Creates the data file if it is missing. + * Raises {@link DataSavingExceptionEvent} if there was an error during saving. + */ + void handleToDoListChangedEvent(ToDoListChangedEvent event); + + /** + * Saves the current version of the alias table in Command Library to the hard disk. + * Creates the data file if it is missing. + * Raises {@link DataSavingExceptionEvent} if there was an error during saving. + */ + void handleAliasTableChangedEvent(AliasTableChangedEvent event); + + /** Loads todo list data from the file **/ + void handleLoadDataRequestEvent(LoadDataRequestEvent event); + + /** Sets the save location **/ + void handleChangeSaveLocationEvent(ChangeSaveLocationEvent event); +} diff --git a/src/main/java/seedu/agendum/storage/StorageManager.java b/src/main/java/seedu/agendum/storage/StorageManager.java new file mode 100644 index 000000000000..98432ee02e34 --- /dev/null +++ b/src/main/java/seedu/agendum/storage/StorageManager.java @@ -0,0 +1,179 @@ +package seedu.agendum.storage; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import seedu.agendum.commons.core.ComponentManager; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.events.model.LoadDataRequestEvent; +import seedu.agendum.commons.events.logic.AliasTableChangedEvent; +import seedu.agendum.commons.events.model.ChangeSaveLocationEvent; +import seedu.agendum.commons.events.model.ToDoListChangedEvent; +import seedu.agendum.commons.events.storage.DataLoadingExceptionEvent; +import seedu.agendum.commons.events.storage.DataSavingExceptionEvent; +import seedu.agendum.commons.events.storage.LoadDataCompleteEvent; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.ConfigUtil; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.model.ReadOnlyToDoList; +import seedu.agendum.model.UserPrefs; + +/** + * Manages storage of ToDoList, UserPrefs, CommandLibrary data in local storage. + */ +public class StorageManager extends ComponentManager implements Storage { + + private static final Logger logger = LogsCenter.getLogger(StorageManager.class); + private ToDoListStorage toDoListStorage; + private UserPrefsStorage userPrefsStorage; + private AliasTableStorage aliasTableStorage; + private Config config; + + public StorageManager(ToDoListStorage toDoListStorage, AliasTableStorage aliasTableStorage, + UserPrefsStorage userPrefsStorage, Config config) { + super(); + this.toDoListStorage = toDoListStorage; + this.aliasTableStorage = aliasTableStorage; + this.userPrefsStorage = userPrefsStorage; + this.config = config; + } + + public StorageManager(String toDoListFilePath, String aliasTableFilePath, + String userPrefsFilePath, Config config) { + this(new XmlToDoListStorage(toDoListFilePath), new JsonAliasTableStorage(aliasTableFilePath), + new JsonUserPrefsStorage(userPrefsFilePath), config); + } + + // ================ Alias Table methods ============================== + + @Override + public Optional> readAliasTable() + throws DataConversionException, IOException { + return aliasTableStorage.readAliasTable(); + } + + @Override + public void saveAliasTable(Hashtable table) throws IOException { + aliasTableStorage.saveAliasTable(table); + } + + // ================ UserPrefs methods ============================== + + @Override + public Optional readUserPrefs() throws DataConversionException, IOException { + return userPrefsStorage.readUserPrefs(); + } + + @Override + public void saveUserPrefs(UserPrefs userPrefs) throws IOException { + userPrefsStorage.saveUserPrefs(userPrefs); + } + + // ================ ToDoList methods ============================== + + @Override + public String getToDoListFilePath() { + return toDoListStorage.getToDoListFilePath(); + } + + @Override + public Optional readToDoList() throws DataConversionException { + return readToDoList(toDoListStorage.getToDoListFilePath()); + } + + @Override + public Optional readToDoList(String filePath) throws DataConversionException { + logger.fine("Attempting to read data from file: " + filePath); + return toDoListStorage.readToDoList(filePath); + } + + @Override + public void saveToDoList(ReadOnlyToDoList toDoList) throws IOException { + saveToDoList(toDoList, toDoListStorage.getToDoListFilePath()); + } + + @Override + public void saveToDoList(ReadOnlyToDoList toDoList, String filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + toDoListStorage.saveToDoList(toDoList, filePath); + } + + //@@author A0148095X + @Override + public void setToDoListFilePath(String filePath){ + assert StringUtil.isValidPathToFile(filePath); + toDoListStorage.setToDoListFilePath(filePath); + logger.info("Setting todo list file path to: " + filePath); + } + + private void saveConfigFile() { + try { + ConfigUtil.saveConfig(config, config.getConfigFilePath()); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + } + + //@@author + @Override + @Subscribe + public void handleToDoListChangedEvent(ToDoListChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local data changed, saving to file")); + try { + saveToDoList(event.data); + } catch (IOException e) { + raise(new DataSavingExceptionEvent(e)); + } + } + + @Override + @Subscribe + public void handleAliasTableChangedEvent(AliasTableChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Alias table changed, saving to file")); + try { + saveAliasTable(event.aliasTable); + } catch (IOException e) { + raise(new DataSavingExceptionEvent(e)); + } + } + + //@@author A0148095X + @Override + @Subscribe + public void handleChangeSaveLocationEvent(ChangeSaveLocationEvent event) { + String location = event.location; + + setToDoListFilePath(location); + config.setToDoListFilePath(location); + saveConfigFile(); + + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + } + + @Override + @Subscribe + public void handleLoadDataRequestEvent(LoadDataRequestEvent event) { + setToDoListFilePath(event.loadLocation); + + Optional toDoListOptional; + ReadOnlyToDoList loadedData = null; + try { + toDoListOptional = readToDoList(); + loadedData = toDoListOptional.get(); + logger.info("Loading successful - " + LogsCenter.getEventHandlingLogMessage(event)); + raise(new LoadDataCompleteEvent(loadedData)); + } catch (DataConversionException dce) { + logger.warning("Loading unsuccessful - Data file not in the correct format. "); + raise(new DataLoadingExceptionEvent(dce)); + } catch (NoSuchElementException nse) { + logger.warning("Loading unsuccessful - File does not exist."); + raise(new DataLoadingExceptionEvent(nse)); + } + } +} diff --git a/src/main/java/seedu/agendum/storage/ToDoListStorage.java b/src/main/java/seedu/agendum/storage/ToDoListStorage.java new file mode 100644 index 000000000000..0278d14182fd --- /dev/null +++ b/src/main/java/seedu/agendum/storage/ToDoListStorage.java @@ -0,0 +1,47 @@ +package seedu.agendum.storage; + +import java.io.IOException; +import java.util.Optional; + +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.model.ReadOnlyToDoList; + +/** + * Represents a storage for {@link seedu.agendum.model.ToDoList}. + */ +public interface ToDoListStorage { + + /** + * Returns the file path of the data file. + */ + String getToDoListFilePath(); + + void setToDoListFilePath(String filePath); + + /** + * Returns ToDoList data as a {@link ReadOnlyToDoList}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readToDoList() throws DataConversionException, IOException; + + /** + * @see #getToDoListFilePath() + */ + Optional readToDoList(String filePath) throws DataConversionException; + + /** + * Saves the given {@link ReadOnlyToDoList} to the storage. + * @param toDoList cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveToDoList(ReadOnlyToDoList toDoList) throws IOException; + + /** + * @see #saveToDoList(ReadOnlyToDoList) + */ + void saveToDoList(ReadOnlyToDoList toDoList, String filePath) throws IOException; + + +} diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/agendum/storage/UserPrefsStorage.java similarity index 73% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/seedu/agendum/storage/UserPrefsStorage.java index ad2dc935187c..1b5ca328f4ff 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/agendum/storage/UserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package seedu.agendum.storage; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.UserPrefs; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.model.UserPrefs; import java.io.IOException; import java.util.Optional; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link seedu.agendum.model.UserPrefs}. */ public interface UserPrefsStorage { @@ -20,7 +20,7 @@ public interface UserPrefsStorage { Optional readUserPrefs() throws DataConversionException, IOException; /** - * Saves the given {@link seedu.address.model.UserPrefs} to the storage. + * Saves the given {@link seedu.agendum.model.UserPrefs} to the storage. * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/seedu/agendum/storage/XmlAdaptedTask.java b/src/main/java/seedu/agendum/storage/XmlAdaptedTask.java new file mode 100644 index 000000000000..69fb21a3b326 --- /dev/null +++ b/src/main/java/seedu/agendum/storage/XmlAdaptedTask.java @@ -0,0 +1,85 @@ +package seedu.agendum.storage; + +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.*; + +import javax.xml.bind.annotation.XmlElement; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +/** + * JAXB-friendly version of the Task. + */ +public class XmlAdaptedTask { + + private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + @XmlElement(required = true) + private String name; + @XmlElement(required = true) + private String isCompleted; + @XmlElement(required = true) + private String lastUpdatedTime; + @XmlElement(required = false) + private String startDateTime; + @XmlElement(required = false) + private String endDateTime; + + /** + * No-arg constructor for JAXB use. + */ + public XmlAdaptedTask() {} + + + /** + * Converts a given Task into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedTask + */ + public XmlAdaptedTask(ReadOnlyTask source) { + name = source.getName().fullName; + isCompleted = Boolean.toString(source.isCompleted()); + lastUpdatedTime = source.getLastUpdatedTime().format(formatter); + + if (source.getStartDateTime().isPresent()) { + startDateTime = source.getStartDateTime().get().format(formatter); + } + + if (source.getEndDateTime().isPresent()) { + endDateTime = source.getEndDateTime().get().format(formatter); + } + } + + /** + * Converts this jaxb-friendly adapted task object into the model's Task object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted task + */ + public Task toModelType() throws IllegalValueException { + final Name name = new Name(this.name); + final boolean markedAsCompleted = Boolean.valueOf(isCompleted); + + Task newTask = new Task(name); + + if (lastUpdatedTime != null) { + newTask.setLastUpdatedTime(LocalDateTime.parse(lastUpdatedTime, formatter)); + } else { + newTask.setLastUpdatedTimeToNow(); + } + + if (markedAsCompleted) { + newTask.markAsCompleted(); + } + + if (startDateTime != null) { + newTask.setStartDateTime(Optional.ofNullable(LocalDateTime.parse(startDateTime, formatter))); + } + + if (endDateTime != null) { + newTask.setEndDateTime(Optional.ofNullable(LocalDateTime.parse(endDateTime, formatter))); + } + + return newTask; + } +} diff --git a/src/main/java/seedu/address/storage/XmlFileStorage.java b/src/main/java/seedu/agendum/storage/XmlFileStorage.java similarity index 57% rename from src/main/java/seedu/address/storage/XmlFileStorage.java rename to src/main/java/seedu/agendum/storage/XmlFileStorage.java index 27a5210cadaf..855b3e14ec71 100644 --- a/src/main/java/seedu/address/storage/XmlFileStorage.java +++ b/src/main/java/seedu/agendum/storage/XmlFileStorage.java @@ -1,35 +1,35 @@ -package seedu.address.storage; +package seedu.agendum.storage; -import seedu.address.commons.util.XmlUtil; -import seedu.address.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.XmlUtil; +import seedu.agendum.commons.exceptions.DataConversionException; import javax.xml.bind.JAXBException; import java.io.File; import java.io.FileNotFoundException; /** - * Stores addressbook data in an XML file + * Stores to do list data in an XML file */ public class XmlFileStorage { /** - * Saves the given addressbook data to the specified file. + * Saves the given to do list data to the specified file. */ - public static void saveDataToFile(File file, XmlSerializableAddressBook addressBook) + public static void saveDataToFile(File file, XmlSerializableToDoList toDoList) throws FileNotFoundException { try { - XmlUtil.saveDataToFile(file, addressBook); + XmlUtil.saveDataToFile(file, toDoList); } catch (JAXBException e) { assert false : "Unexpected exception " + e.getMessage(); } } /** - * Returns address book in the file or an empty address book + * Returns to do list in the file or an empty to do list */ - public static XmlSerializableAddressBook loadDataFromSaveFile(File file) throws DataConversionException, + public static XmlSerializableToDoList loadDataFromSaveFile(File file) throws DataConversionException, FileNotFoundException { try { - return XmlUtil.getDataFromFile(file, XmlSerializableAddressBook.class); + return XmlUtil.getDataFromFile(file, XmlSerializableToDoList.class); } catch (JAXBException e) { throw new DataConversionException(e); } diff --git a/src/main/java/seedu/agendum/storage/XmlSerializableToDoList.java b/src/main/java/seedu/agendum/storage/XmlSerializableToDoList.java new file mode 100644 index 000000000000..61d21c01d3ce --- /dev/null +++ b/src/main/java/seedu/agendum/storage/XmlSerializableToDoList.java @@ -0,0 +1,65 @@ +package seedu.agendum.storage; + +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.UniqueTaskList; +import seedu.agendum.model.ReadOnlyToDoList; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * An Immutable ToDoList that is serializable to XML format + */ +@XmlRootElement(name = "todolist") +public class XmlSerializableToDoList implements ReadOnlyToDoList { + + @XmlElement + private List tasks; + + { + tasks = new ArrayList<>(); + } + + /** + * Empty constructor required for marshalling + */ + public XmlSerializableToDoList() {} + + /** + * Conversion + */ + public XmlSerializableToDoList(ReadOnlyToDoList src) { + tasks.addAll(src.getTaskList().stream().map(XmlAdaptedTask::new).collect(Collectors.toList())); + } + + @Override + public UniqueTaskList getUniqueTaskList() { + UniqueTaskList lists = new UniqueTaskList(); + for (XmlAdaptedTask p : tasks) { + try { + lists.add(p.toModelType()); + } catch (IllegalValueException e) { + //TODO: better error handling + } + } + return lists; + } + + @Override + public List getTaskList() { + return tasks.stream().map(p -> { + try { + return p.toModelType(); + } catch (IllegalValueException e) { + e.printStackTrace(); + //TODO: better error handling + return null; + } + }).collect(Collectors.toCollection(ArrayList::new)); + } + +} diff --git a/src/main/java/seedu/agendum/storage/XmlToDoListStorage.java b/src/main/java/seedu/agendum/storage/XmlToDoListStorage.java new file mode 100644 index 000000000000..e7b1e320f3ec --- /dev/null +++ b/src/main/java/seedu/agendum/storage/XmlToDoListStorage.java @@ -0,0 +1,80 @@ +package seedu.agendum.storage; + +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.model.ReadOnlyToDoList; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * A class to access ToDoList data stored as an xml file on the hard disk. + */ +public class XmlToDoListStorage implements ToDoListStorage { + + private static final Logger logger = LogsCenter.getLogger(XmlToDoListStorage.class); + + private String filePath; + + public XmlToDoListStorage(String filePath){ + this.filePath = filePath; + } + + public String getToDoListFilePath(){ + return filePath; + } + + /** + * Similar to {@link #readToDoList()} + * @param filePath location of the data. Cannot be null + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readToDoList(String filePath) throws DataConversionException { + assert filePath != null; + File toDoListFile = new File(filePath); + + ReadOnlyToDoList toDoListOptional; + try { + toDoListOptional = XmlFileStorage.loadDataFromSaveFile(new File(filePath)); + } catch (FileNotFoundException e) { + logger.info("ToDoList file " + toDoListFile + " not found"); + return Optional.empty(); + } + + return Optional.of(toDoListOptional); + } + + /** + * Similar to {@link #saveToDoList(ReadOnlyToDoList)} + * @param filePath location of the data. Cannot be null + */ + public void saveToDoList(ReadOnlyToDoList toDoList, String filePath) throws IOException { + assert toDoList != null; + assert filePath != null; + + File file = new File(filePath); + FileUtil.createIfMissing(file); + XmlFileStorage.saveDataToFile(file, new XmlSerializableToDoList(toDoList)); + } + + @Override + public Optional readToDoList() throws DataConversionException, IOException { + return readToDoList(filePath); + } + + @Override + public void saveToDoList(ReadOnlyToDoList toDoList) throws IOException { + saveToDoList(toDoList, filePath); + } + + @Override + public void setToDoListFilePath(String filePath) { + assert StringUtil.isValidPathToFile(filePath); + this.filePath = filePath; + } +} diff --git a/src/main/java/seedu/agendum/sync/Sync.java b/src/main/java/seedu/agendum/sync/Sync.java new file mode 100644 index 000000000000..8c945edb754a --- /dev/null +++ b/src/main/java/seedu/agendum/sync/Sync.java @@ -0,0 +1,30 @@ +package seedu.agendum.sync; + +import seedu.agendum.model.task.Task; + +//@@author A0003878Y +public interface Sync { + + /** Enum used to persist SyncManager status **/ + enum SyncStatus { + RUNNING, NOTRUNNING + } + + /** Retrieve sync manager sync status **/ + SyncStatus getSyncStatus(); + + /** Sets sync manager sync status **/ + void setSyncStatus(SyncStatus syncStatus); + + /** Turn on syncing **/ + void startSyncing(); + + /** Turn off syncing **/ + void stopSyncing(); + + /** Add Task to sync provider **/ + void addNewEvent(Task task); + + /** Remove task from sync provider **/ + void deleteEvent(Task task); +} diff --git a/src/main/java/seedu/agendum/sync/SyncManager.java b/src/main/java/seedu/agendum/sync/SyncManager.java new file mode 100644 index 000000000000..4c4b0bf8efaf --- /dev/null +++ b/src/main/java/seedu/agendum/sync/SyncManager.java @@ -0,0 +1,58 @@ +package seedu.agendum.sync; + +import seedu.agendum.commons.core.ComponentManager; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.model.task.Task; + +import java.util.logging.Logger; + +//@@author A0003878Y +public class SyncManager extends ComponentManager implements Sync { + private final Logger logger = LogsCenter.getLogger(SyncManager.class); + private SyncStatus syncStatus = SyncStatus.NOTRUNNING; + + private final SyncProvider syncProvider; + + public SyncManager(SyncProvider syncProvider) { + this.syncProvider = syncProvider; + this.syncProvider.setManager(this); + + syncProvider.startIfNeeded(); + } + + @Override + public SyncStatus getSyncStatus() { + return syncStatus; + } + + @Override + public void setSyncStatus(SyncStatus syncStatus) { + this.syncStatus = syncStatus; + } + + @Override + public void startSyncing() { + syncProvider.start(); + } + + @Override + public void stopSyncing() { + syncProvider.stop(); + } + + @Override + public void addNewEvent(Task task) { + if (syncStatus == SyncStatus.RUNNING) { + if (task.getStartDateTime().isPresent() && task.getEndDateTime().isPresent()) { + syncProvider.addNewEvent(task); + } + } + } + + @Override + public void deleteEvent(Task task) { + if (syncStatus == SyncStatus.RUNNING) { + syncProvider.deleteEvent(task); + } + } +} diff --git a/src/main/java/seedu/agendum/sync/SyncProvider.java b/src/main/java/seedu/agendum/sync/SyncProvider.java new file mode 100644 index 000000000000..1e20639b408b --- /dev/null +++ b/src/main/java/seedu/agendum/sync/SyncProvider.java @@ -0,0 +1,31 @@ +package seedu.agendum.sync; + +import seedu.agendum.model.task.Task; + +//@@author A0003878Y +public abstract class SyncProvider { + + /** Sync provider's keep a reference to the manager so that they can set it's + * sync status **/ + protected Sync syncManager; + + /** Start sync provider and perform initialization **/ + public abstract void start(); + + /** Start sync provider if it needs to be started **/ + public abstract void startIfNeeded(); + + /** Stop sync provider and perform cleanup **/ + public abstract void stop(); + + /** Add event into sync provider **/ + public abstract void addNewEvent(Task task); + + /** Delete event from sync provider **/ + public abstract void deleteEvent(Task task); + + /** Set sync provider's sync manager **/ + public void setManager(Sync syncManager) { + this.syncManager = syncManager; + } +} diff --git a/src/main/java/seedu/agendum/sync/SyncProviderGoogle.java b/src/main/java/seedu/agendum/sync/SyncProviderGoogle.java new file mode 100644 index 000000000000..20cbe49534d0 --- /dev/null +++ b/src/main/java/seedu/agendum/sync/SyncProviderGoogle.java @@ -0,0 +1,241 @@ +package seedu.agendum.sync; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.DateTime; +import com.google.api.client.util.store.FileDataStoreFactory; +import com.google.api.services.calendar.model.*; +import com.google.api.services.calendar.model.Calendar; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.model.task.Task; + +import java.io.File; +import java.io.IOException; +import java.time.ZoneId; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import static java.lang.Math.abs; +import static seedu.agendum.commons.core.Config.DEFAULT_DATA_DIR; + +//@@author A0003878Y +public class SyncProviderGoogle extends SyncProvider { + private final Logger logger = LogsCenter.getLogger(SyncProviderGoogle.class); + + private static final String CALENDAR_NAME = "Agendum Calendar"; + private static final File DATA_STORE_DIR = new File(DEFAULT_DATA_DIR); + private static final File DATA_STORE_CREDENTIAL = new File(DEFAULT_DATA_DIR + "StoredCredential"); + private static final String CLIENT_ID = "1011464737889-n9avi9id8fur78jh3kqqctp9lijphq2n.apps.googleusercontent.com"; + private static final String CLIENT_SECRET = "ea78y_rPz3G4kwIV3yAF99aG"; + private static FileDataStoreFactory dataStoreFactory; + private static HttpTransport httpTransport; + private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + private static com.google.api.services.calendar.Calendar client; + + private Calendar agendumCalendar; + + // These are blocking queues to ease the producer/consumer problem + private static final ArrayBlockingQueue addEventConcurrentQueue = new ArrayBlockingQueue(200); + private static final ArrayBlockingQueue deleteEventConcurrentQueue = new ArrayBlockingQueue(200); + + public SyncProviderGoogle() { + try { + httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR); + } catch (IOException var3) { + System.err.println(var3.getMessage()); + } catch (Throwable var4) { + var4.printStackTrace(); + } + } + + @Override + public void start() { + logger.info("Initializing Google Calendar Sync"); + try { + Credential t = authorize(); + client = (new com.google.api.services.calendar.Calendar.Builder(httpTransport, JSON_FACTORY, t)).setApplicationName("Agendum").build(); + agendumCalendar = getAgendumCalendar(); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + + // Process add & delete consumers into their own separate thread. + Executors.newSingleThreadExecutor().execute(() -> processAddEventQueue()); + Executors.newSingleThreadExecutor().execute(() -> processDeleteEventQueue()); + } catch (IOException var3) { + System.err.println(var3.getMessage()); + } catch (Throwable var4) { + var4.printStackTrace(); + } + } + + @Override + public void startIfNeeded() { + logger.info("Checking if Google Calendar needs to be started"); + if (DATA_STORE_CREDENTIAL.exists()) { + logger.info("Credentials, starting Google Calendar"); + start(); + } + } + + @Override + public void stop() { + logger.info("Stopping Google Calendar Sync"); + DATA_STORE_CREDENTIAL.delete(); + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + } + + @Override + public void addNewEvent(Task task) { + try { + addEventConcurrentQueue.put(task); + logger.info("Task added to GCal add queue"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public void deleteEvent(Task task) { + try { + deleteEventConcurrentQueue.put(task); + logger.info("Task added to GCal delete queue"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Authorize with Google Calendar + * @return Credentail + * @throws Exception + */ + private Credential authorize() throws Exception { + GoogleClientSecrets.Details details = new GoogleClientSecrets.Details(); + details.setClientId(CLIENT_ID); + details.setClientSecret(CLIENT_SECRET); + + GoogleClientSecrets clientSecrets = new GoogleClientSecrets().setInstalled(details); + + GoogleAuthorizationCodeFlow flow = (new GoogleAuthorizationCodeFlow.Builder(httpTransport, JSON_FACTORY, clientSecrets, Collections.singleton("https://www.googleapis.com/auth/calendar"))).setDataStoreFactory(dataStoreFactory).build(); + return (new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver())).authorize("user"); + } + + /** + * Returns a new "Agendum Calendar" in the authenticated user. + * If a calendar with the same name doesn't already exist, it creates one. + * @return + * @throws IOException + */ + private Calendar getAgendumCalendar() throws IOException { + CalendarList feed = client.calendarList().list().execute(); + logger.info("Searching for Agnendum Calendar"); + + for (CalendarListEntry entry : feed.getItems()) { + if (entry.getSummary().equals(CALENDAR_NAME)) { + logger.info(CALENDAR_NAME + " found"); + Calendar calendar = client.calendars().get(entry.getId()).execute(); + logger.info(calendar.toPrettyString()); + return calendar; + } + + } + + logger.info(CALENDAR_NAME + " not found, creating it"); + Calendar entry = new Calendar(); + entry.setSummary(CALENDAR_NAME); + Calendar calendar = client.calendars().insert(entry).execute(); + logger.info(calendar.toPrettyString()); + return calendar; + } + + /** + * Delete Agendum calendar in Google Calendar. + */ + public void deleteAgendumCalendar() { + try { + CalendarList feed = client.calendarList().list().execute(); + logger.info("Deleting Agendum calendar"); + + for (CalendarListEntry entry : feed.getItems()) { + if (entry.getSummary().equals(CALENDAR_NAME)) { + client.calendars().delete(entry.getId()).execute(); + } + + } + } catch (IOException e) + {e.printStackTrace(); + } + } + + /** + * A event loop that continuously processes the add event queue. + * + * `.take();` is a blocking call so it waits until there is something + * in the array before returning. + * + * This method should only be called on non-main thread. + */ + private void processAddEventQueue() { + while (true) { + try { + Task task = addEventConcurrentQueue.take(); + Date startDate = Date.from(task.getStartDateTime().get().atZone(ZoneId.systemDefault()).toInstant()); + Date endDate = Date.from(task.getEndDateTime().get().atZone(ZoneId.systemDefault()).toInstant()); + String id = Integer.toString(abs(task.syncCode())); + + EventDateTime startEventDateTime = new EventDateTime().setDateTime(new DateTime(startDate)); + EventDateTime endEventDateTime = new EventDateTime().setDateTime(new DateTime(endDate)); + + Event newEvent = new Event(); + newEvent.setSummary(String.valueOf(task.getName())); + newEvent.setStart(startEventDateTime); + newEvent.setEnd(endEventDateTime); + newEvent.setId(id); + + Event result = client.events().insert(agendumCalendar.getId(), newEvent).execute(); + logger.info(result.toPrettyString()); + + logger.info("Task processed from GCal add queue"); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * A event loop that continuously processes the delete event queue. + * + * `.take();` is a blocking call so it waits until there is something + * in the array before returning. + * + * This method should only be called on non-main thread. + */ + private void processDeleteEventQueue() { + while (true) { + try { + Task task = deleteEventConcurrentQueue.take(); + String id = Integer.toString(abs(task.syncCode())); + client.events().delete(agendumCalendar.getId(), id).execute(); + + logger.info("Task added to GCal delete queue"); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/seedu/agendum/ui/CommandBox.java b/src/main/java/seedu/agendum/ui/CommandBox.java new file mode 100644 index 000000000000..2e0574cc0cc2 --- /dev/null +++ b/src/main/java/seedu/agendum/ui/CommandBox.java @@ -0,0 +1,210 @@ +package seedu.agendum.ui; + +import com.google.common.eventbus.Subscribe; + +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import seedu.agendum.commons.events.ui.CloseHelpWindowRequestEvent; +import seedu.agendum.commons.events.ui.IncorrectCommandAttemptedEvent; +import seedu.agendum.logic.Logic; +import seedu.agendum.logic.commands.*; +import seedu.agendum.logic.parser.EditDistanceCalculator; +import seedu.agendum.commons.util.FxViewUtil; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.core.Messages; + +import java.util.Optional; +import java.util.logging.Logger; + +//@@author A0148031R +/** + * Controller for the command box field + * + */ +public class CommandBox extends UiPart { + private final Logger logger = LogsCenter.getLogger(CommandBox.class); + private static final String FXML = "CommandBox.fxml"; + private static final String FIND_COMMAND = "find "; + private static final String HELP_COMMAND = "help"; + private static final String RESULT_FEEDBACK = "Result: "; + private static final String ERROR = "error"; + private static final Color MESSAGE_COLOR = Color.web("#ffffff"); + + private AnchorPane placeHolderPane; + private AnchorPane commandPane; + private StackPane messagePlaceHolder; + private ResultPopUp resultPopUp; + private static CommandBoxHistory commandBoxHistory; + + private Logic logic; + + @FXML + private TextField commandTextField; + private CommandResult mostRecentResult; + + public static CommandBox load(Stage primaryStage, AnchorPane commandBoxPlaceholder, StackPane messagePlaceHolder, + ResultPopUp resultPopUp, Logic logic) { + CommandBox commandBox = UiPartLoader.loadUiPart(primaryStage, commandBoxPlaceholder, new CommandBox()); + commandBox.configure(resultPopUp, messagePlaceHolder, logic); + commandBox.addToPlaceholder(); + commandBoxHistory = CommandBoxHistory.getInstance(); + return commandBox; + } + + public void configure(ResultPopUp resultPopUp, StackPane messagePlaceHolder, Logic logic) { + this.resultPopUp = resultPopUp; + this.messagePlaceHolder = messagePlaceHolder; + this.logic = logic; + registerAsAnEventHandler(this); + registerArrowKeyEventFilter(); + registerTabKeyEventFilter(); + } + + private void addToPlaceholder() { + SplitPane.setResizableWithParent(placeHolderPane, false); + FxViewUtil.applyAnchorBoundaryParameters(commandPane, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(commandTextField, 0.0, 0.0, 0.0, 0.0); + placeHolderPane.getChildren().add(commandTextField); + } + + @Override + public void setNode(Node node) { + commandPane = (AnchorPane) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + public void setPlaceholder(AnchorPane pane) { + this.placeHolderPane = pane; + } + + /** + * Executes the command and saves this command to history if comamnd input + * is changed + */ + @FXML + private void handleCommandInputChanged() { + //Take a copy of the command text + commandBoxHistory.saveNewCommand(commandTextField.getText()); + String previousCommandTest = commandBoxHistory.getLastCommand(); + if(previousCommandTest.toLowerCase().trim().startsWith(FIND_COMMAND) && + previousCommandTest.toLowerCase().trim().length() > FIND_COMMAND.length()) { + postMessage(Messages.MESSAGE_ESCAPE_HELP_WINDOW); + } else { + raise(new CloseHelpWindowRequestEvent()); + } + + /* We assume the command is correct. If it is incorrect, the command box will be changed accordingly + * in the event handling code {@link #handleIncorrectCommandAttempted} + */ + + setStyleToIndicateCorrectCommand(); + mostRecentResult = logic.execute(previousCommandTest); + if(!previousCommandTest.toLowerCase().equals(HELP_COMMAND)) { + resultPopUp.postMessage(mostRecentResult.feedbackToUser); + } + logger.info(RESULT_FEEDBACK + mostRecentResult.feedbackToUser); + } + + /** + * Post meesage in the message place holder under the command box + */ + private void postMessage(String message) { + this.messagePlaceHolder.getChildren().clear(); + raise(new CloseHelpWindowRequestEvent()); + + Label label = new Label(message); + label.setTextFill(MESSAGE_COLOR); + label.setContentDisplay(ContentDisplay.CENTER); + label.setPadding(new Insets(0, 10, 0, 10)); + this.messagePlaceHolder.setAlignment(Pos.CENTER_LEFT); + this.messagePlaceHolder.getChildren().add(label); + } + + /** + * Sets arrow key for scrolling through command history + */ + private void registerArrowKeyEventFilter() { + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + KeyCode keyCode = event.getCode(); + if (keyCode.equals(KeyCode.UP)) { + String previousCommand = commandBoxHistory.getPreviousCommand(); + commandTextField.setText(previousCommand); + } else if (keyCode.equals(KeyCode.DOWN)) { + String nextCommand = commandBoxHistory.getNextCommand(); + commandTextField.setText(nextCommand); + } else { + return; + } + commandTextField.end(); + event.consume(); + }); + } + + /** + * Sets tab key for autocomplete + */ + private void registerTabKeyEventFilter() { + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + KeyCode keyCode = event.getCode(); + if (keyCode.equals(KeyCode.TAB)) { + Optional parsedString = EditDistanceCalculator.findCommandCompletion(commandTextField.getText()); + if(parsedString.isPresent()) { + commandTextField.setText(parsedString.get()); + } + } else { + return; + } + commandTextField.end(); + event.consume(); + }); + } + + /** + * Sets the command box style to indicate a correct command. + */ + private void setStyleToIndicateCorrectCommand() { + commandTextField.getStyleClass().remove("error"); + commandTextField.setText(""); + } + + @Subscribe + private void handleIncorrectCommandAttempted(IncorrectCommandAttemptedEvent event){ + logger.info(LogsCenter.getEventHandlingLogMessage( + event, "Invalid command: " + commandBoxHistory.getLastCommand())); + setStyleToIndicateIncorrectCommand(); + restoreCommandText(); + } + + /** + * Restores the command box text to the previously entered command + */ + private void restoreCommandText() { + commandTextField.setText(commandBoxHistory.getLastCommand()); + commandTextField.selectEnd(); + } + + /** + * Sets the command box style to indicate an error + */ + private void setStyleToIndicateIncorrectCommand() { + commandTextField.getStyleClass().add(ERROR); + } + +} diff --git a/src/main/java/seedu/agendum/ui/CommandBoxHistory.java b/src/main/java/seedu/agendum/ui/CommandBoxHistory.java new file mode 100644 index 000000000000..0a18b5e93aea --- /dev/null +++ b/src/main/java/seedu/agendum/ui/CommandBoxHistory.java @@ -0,0 +1,87 @@ +package seedu.agendum.ui; + +import java.util.LinkedList; +import java.util.ListIterator; + +//@@author A0148031R +/** + * Stores previous valid and invalid commands in a linked list with a max size + * New commands are added to the head of the linked list + */ +public class CommandBoxHistory { + + private static final int MAX_PREVIOUS_LINES = 15; + private static final String PREVIOUS_QUERY = "previous"; + private static final String NEXT_QUERY = "next"; + private static final String EMPTY_QUERY = ""; + private static final String EMPTY_COMMAND = ""; + private final LinkedList pastCommands; + private ListIterator iterator; + private String lastCommand = ""; + private String lastQuery = EMPTY_QUERY; + + private static CommandBoxHistory instance = null; + + private CommandBoxHistory() { + pastCommands = new LinkedList<>(); + iterator = pastCommands.listIterator(); + } + + public static CommandBoxHistory getInstance() { + if(instance == null) { + instance = new CommandBoxHistory(); + } + return instance; + } + + public String getLastCommand() { + return lastCommand; + } + + /** + * Retrieves the previous valid/invalid command. + * If there is no previous command, returns an empty string to clear the command box + */ + public String getPreviousCommand() { + if(!iterator.hasNext()) { + lastQuery = EMPTY_QUERY; + return EMPTY_COMMAND; + } else if(lastQuery.equals(NEXT_QUERY)) { + iterator.next(); + } + lastQuery = PREVIOUS_QUERY; + return iterator.next(); + } + + /** + * Retrieves the next valid/invalid command. + * If there is no next command, return an empty string to clear the command box + */ + public String getNextCommand() { + if (!iterator.hasPrevious()) { + lastQuery = EMPTY_QUERY; + return EMPTY_COMMAND; + } else if(lastQuery.equals(PREVIOUS_QUERY)) { + iterator.previous(); + } + lastQuery = NEXT_QUERY; + return iterator.previous(); + + } + + /** + * Takes in the latest command string entered and add it to command box history. + * Updates the iterator to point to the latest element + */ + public void saveNewCommand(String newCommand) { + lastCommand = newCommand; + pastCommands.addFirst(lastCommand); + + if (pastCommands.size() > MAX_PREVIOUS_LINES) { + pastCommands.removeLast(); + } + + iterator = pastCommands.listIterator(); + } + +} diff --git a/src/main/java/seedu/agendum/ui/CompletedTasksPanel.java b/src/main/java/seedu/agendum/ui/CompletedTasksPanel.java new file mode 100644 index 000000000000..2b3f7b89c98e --- /dev/null +++ b/src/main/java/seedu/agendum/ui/CompletedTasksPanel.java @@ -0,0 +1,92 @@ +package seedu.agendum.ui; + +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.event.Event; +import javafx.fxml.FXML; +import javafx.scene.control.Control; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.MultipleSelectionModel; +import javafx.scene.control.SelectionMode; +import javafx.scene.input.MouseEvent; +import javafx.util.Duration; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; + +//@@author A0148031R +/** + * Panel contains the list of completed tasks + */ +public class CompletedTasksPanel extends TasksPanel { + private static final String FXML = "CompletedTasksPanel.fxml"; + private static ObservableList mainTaskList; + private MultipleSelectionModel selectionModel; + + @FXML + private ListView completedTasksListView; + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + protected void setConnections(ObservableList taskList) { + mainTaskList = taskList; + completedTasksListView.setItems(taskList.filtered(ReadOnlyTask::isCompleted)); + completedTasksListView.setCellFactory(listView -> new CompletedTasksListViewCell()); + configure(); + } + + private void configure() { + selectionModel = completedTasksListView.getSelectionModel(); + completedTasksListView.setSelectionModel(null); + completedTasksListView.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume); + } + + /** + * Scrolls to the newly updated task and highlight for several seconds. If + * there are multiple tasks updated, previous highlight will not be cleared. + */ + public void scrollTo(Task task, boolean hasMultipleTasks) { + Platform.runLater(() -> { + + int index = mainTaskList.indexOf(task) - mainTaskList.filtered(t -> !t.isCompleted()).size(); + completedTasksListView.scrollTo(index); + completedTasksListView.setSelectionModel(selectionModel); + + if(hasMultipleTasks) { + completedTasksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + completedTasksListView.getSelectionModel().select(index); + } else { + completedTasksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + completedTasksListView.getSelectionModel().clearAndSelect(index); + } + + PauseTransition delay = new PauseTransition(Duration.seconds(5)); + delay.setOnFinished(event -> completedTasksListView.getSelectionModel().clearSelection(index)); + delay.play(); + }); + } + + class CompletedTasksListViewCell extends ListCell { + public CompletedTasksListViewCell() { + prefWidthProperty().bind(completedTasksListView.widthProperty()); + setMaxWidth(Control.USE_PREF_SIZE); + } + + @Override + protected void updateItem(ReadOnlyTask task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(TaskCard.load(task, mainTaskList.indexOf(task) + 1).getLayout()); + } + } + } +} diff --git a/src/main/java/seedu/agendum/ui/FloatingTasksPanel.java b/src/main/java/seedu/agendum/ui/FloatingTasksPanel.java new file mode 100644 index 000000000000..22f2b4117cbc --- /dev/null +++ b/src/main/java/seedu/agendum/ui/FloatingTasksPanel.java @@ -0,0 +1,93 @@ +package seedu.agendum.ui; + +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.event.Event; +import javafx.fxml.FXML; +import javafx.scene.control.Control; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.MultipleSelectionModel; +import javafx.scene.control.SelectionMode; +import javafx.scene.input.MouseEvent; +import javafx.util.Duration; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; + +//@@author A0148031R +/** + * Panel contains the list of uncompleted floating tasks + */ +public class FloatingTasksPanel extends TasksPanel { + private static final String FXML = "FloatingTasksPanel.fxml"; + private static ObservableList mainTaskList; + private MultipleSelectionModel selectionModel; + + @FXML + private ListView floatingTasksListView; + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + protected void setConnections(ObservableList taskList) { + mainTaskList = taskList; + floatingTasksListView.setItems(taskList.filtered(task -> !task.isCompleted() && !task.hasTime())); + floatingTasksListView.setCellFactory(listView -> new FloatingTasksListViewCell()); + configure(); + } + + private void configure() { + selectionModel = floatingTasksListView.getSelectionModel(); + floatingTasksListView.setSelectionModel(null); + floatingTasksListView.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume); + } + + /** + * Scrolls to the newly updated task and highlight for several seconds. If + * there are multiple tasks updated, previous highlight will not be cleared. + */ + public void scrollTo(Task task, boolean hasMultipleTasks) { + Platform.runLater(() -> { + + int index = mainTaskList.indexOf(task) - + mainTaskList.filtered(t -> (t.hasTime() && !t.isCompleted())).size(); + floatingTasksListView.scrollTo(index); + floatingTasksListView.setSelectionModel(selectionModel); + + if(hasMultipleTasks) { + floatingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + floatingTasksListView.getSelectionModel().select(index); + } else { + floatingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + floatingTasksListView.getSelectionModel().clearAndSelect(index); + } + + PauseTransition delay = new PauseTransition(Duration.seconds(4)); + delay.setOnFinished(event -> floatingTasksListView.getSelectionModel().clearSelection(index)); + delay.play(); + }); + } + + class FloatingTasksListViewCell extends ListCell { + public FloatingTasksListViewCell() { + prefWidthProperty().bind(floatingTasksListView.widthProperty()); + setMaxWidth(Control.USE_PREF_SIZE); + } + + @Override + protected void updateItem(ReadOnlyTask task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(TaskCard.load(task, mainTaskList.indexOf(task) + 1).getLayout()); + } + } + } +} diff --git a/src/main/java/seedu/agendum/ui/HelpWindow.java b/src/main/java/seedu/agendum/ui/HelpWindow.java new file mode 100644 index 000000000000..b48be9827bdd --- /dev/null +++ b/src/main/java/seedu/agendum/ui/HelpWindow.java @@ -0,0 +1,137 @@ +package seedu.agendum.ui; + +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import org.reflections.Reflections; +import seedu.agendum.logic.commands.Command; +import seedu.agendum.commons.core.LogsCenter; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +//@@author A0148031R +/** + * Controller for help anchorpane + */ +public class HelpWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); + private static final String FXML = "HelpWindow.fxml"; + private static final int PADDING = 11; + private static final double COMMAND_COLUMN_WIDTH = 0.2; + private static final double DESCRIPTION_COLUMN_WIDTH = 0.4; + private static final double FORMAT_COLUMN_WIDTH = 0.4; + private final ObservableList> commandList = FXCollections.observableArrayList(); + + private enum CommandColumns { + COMMAND, DESCRIPTION, FORMAT + } + + @FXML + private AnchorPane helpWindowRoot; + + @FXML + private TableView> commandTable; + + @FXML + private TableColumn, String> commandColumn; + + @FXML + private TableColumn, String> descriptionColumn; + + @FXML + private TableColumn, String> formatColumn; + + private StackPane messagePlaceHolder; + private AnchorPane mainPane; + + /** + * Initializes the controller class. This method is automatically called + * after the fxml file has been loaded. + */ + @FXML + private void initialize() { + + commandColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().get(CommandColumns.COMMAND))); + descriptionColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().get(CommandColumns.DESCRIPTION))); + formatColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().get(CommandColumns.FORMAT))); + commandTable.setItems(commandList); + commandTable.setEditable(false); + } + + public static HelpWindow load(Stage primaryStage, StackPane messagePlaceHolder) { + logger.fine("Showing help page about the application."); + HelpWindow helpWindow = UiPartLoader.loadUiPart(primaryStage, new HelpWindow()); + helpWindow.configure(messagePlaceHolder); + return helpWindow; + } + + @Override + public void setNode(Node node) { + this.mainPane = (AnchorPane)node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + public AnchorPane getMainPane() { + return this.mainPane; + } + + private void configure(StackPane messagePlaceHolder){ + this.messagePlaceHolder = messagePlaceHolder; + commandColumn.prefWidthProperty().bind(commandTable.widthProperty().multiply(COMMAND_COLUMN_WIDTH)); + descriptionColumn.prefWidthProperty().bind(commandTable.widthProperty().multiply(DESCRIPTION_COLUMN_WIDTH)); + formatColumn.prefWidthProperty().bind(commandTable.widthProperty().multiply(FORMAT_COLUMN_WIDTH)); + loadHelpList(); + } + + public void show(double height) { + this.messagePlaceHolder.setPadding(new Insets(PADDING)); + this.helpWindowRoot.setMinHeight(height); + this.messagePlaceHolder.setPrefSize(Control.USE_COMPUTED_SIZE, Control.USE_COMPUTED_SIZE); + this.messagePlaceHolder.getChildren().add(helpWindowRoot); + } + + //@@author A0003878Y + + /** + * Uses Java reflection followed by Java stream.map() to retrieve all commands for listing on the Help + * window dynamically + */ + private void loadHelpList() { + new Reflections("seedu.agendum").getSubTypesOf(Command.class) + .stream() + .map(s -> { + try { + Map map = new HashMap<>(); + map.put(CommandColumns.COMMAND, s.getMethod("getName").invoke(null).toString()); + map.put(CommandColumns.FORMAT, s.getMethod("getFormat").invoke(null).toString()); + map.put(CommandColumns.DESCRIPTION, s.getMethod("getDescription").invoke(null).toString()); + return map; + } catch (NullPointerException e) { + return null; // Suppress this exception are we expect some Commands to not conform to these methods + } catch (Exception e) { + logger.severe("Java reflection for Command class failed"); + throw new RuntimeException(); + } + }) + .filter(p -> p != null) // remove nulls + .sorted((lhs, rhs) -> lhs.get(CommandColumns.COMMAND).compareTo(rhs.get(CommandColumns.COMMAND))) + .forEach(m -> commandList.add(m)); + } +} diff --git a/src/main/java/seedu/agendum/ui/MainWindow.java b/src/main/java/seedu/agendum/ui/MainWindow.java new file mode 100644 index 000000000000..78490374c07f --- /dev/null +++ b/src/main/java/seedu/agendum/ui/MainWindow.java @@ -0,0 +1,277 @@ +package seedu.agendum.ui; + +import java.util.logging.Logger; + +import javafx.application.Platform; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SplitPane; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.core.GuiSettings; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.events.ui.ExitAppRequestEvent; +import seedu.agendum.logic.Logic; +import seedu.agendum.model.UserPrefs; + +// @@author A0148031R +/** + * The Main Window. Provides the basic application layout containing a menu bar + * and space where other JavaFX elements can be placed. + */ +public class MainWindow extends UiPart { + private static final Logger logger = LogsCenter.getLogger(MainWindow.class); + + private static final String ICON = "/images/agendum_icon.png"; + private static final String FXML = "MainWindow.fxml"; + private static final String LIST_COMMAND = "list"; + private static final String UNDO_COMMAND = "undo"; + + private Logic logic; + + // Independent Ui parts residing in this Ui container + private TasksPanel upcomingTasksPanel; + private TasksPanel completedTasksPanel; + private TasksPanel floatingTasksPanel; + private AnchorPane helpWindow; + private ResultPopUp resultPopUp; + private StatusBarFooter statusBarFooter; + private CommandBox commandBox; + private Config config; + private UserPrefs userPrefs; + + // Handles to elements of this Ui container + private VBox rootLayout; + private Scene scene; + + @FXML + private AnchorPane browserPlaceholder; + + @FXML + private AnchorPane commandBoxPlaceholder; + + @FXML + private MenuItem helpMenuItem; + + @FXML + private SplitPane splitPane; + + @FXML + private AnchorPane upcomingTasksPlaceHolder; + + @FXML + private AnchorPane completedTasksPlaceHolder; + + @FXML + private AnchorPane floatingTasksPlaceHolder; + + @FXML + private AnchorPane statusbarPlaceholder; + + @FXML + private StackPane messagePlaceHolder; + + public MainWindow() { + super(); + } + + @Override + public void setNode(Node node) { + rootLayout = (VBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + public static MainWindow load(Stage primaryStage, Config config, UserPrefs prefs, Logic logic) { + MainWindow mainWindow = UiPartLoader.loadUiPart(primaryStage, new MainWindow()); + mainWindow.configure(config.getAppTitle(), config, prefs, logic); + return mainWindow; + } + + private void configure(String appTitle, Config config, UserPrefs prefs, Logic logic) { + + this.logic = logic; + this.config = config; + this.userPrefs = prefs; + + setTitle(appTitle); + setIcon(ICON); + setWindowDefaultSize(prefs); + scene = new Scene(rootLayout); + primaryStage.setScene(scene); + primaryStage.setOnCloseRequest(e -> Platform.exit()); + configureEscape(); + configureHelpWindowToggle(); + } + + /** + * Set shortcut key to switch between help window and main window + */ + private void configureHelpWindowToggle() { + scene.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler() { + KeyCombination toggleHelpWindow = new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN); + KeyCombination undo = new KeyCodeCombination(KeyCode.Z, KeyCombination.CONTROL_DOWN); + @Override + public void handle(KeyEvent evt) { + if (toggleHelpWindow.match(evt) && messagePlaceHolder.getChildren().size() == 0) { + openHelpWindow(); + } else if (toggleHelpWindow.match(evt) && messagePlaceHolder.getChildren().contains(helpWindow)) { + closeHelpWindow(); + } else if(undo.match(evt)) { + logic.execute(UNDO_COMMAND); + } + } + }); + } + + /** + * Set shortcut key to quickly switch back to main list after using find + * command or showing help page + */ + private void configureEscape() { + scene.addEventFilter(KeyEvent.KEY_PRESSED, evt -> { + if (evt.getCode().equals(KeyCode.ESCAPE) && messagePlaceHolder.getChildren().contains(helpWindow)) { + closeHelpWindow(); + } else if(evt.getCode().equals(KeyCode.ESCAPE) && messagePlaceHolder.getChildren().size() > 0) { + messagePlaceHolder.getChildren().clear(); + messagePlaceHolder.setMaxHeight(0); + logic.execute(LIST_COMMAND); + } + }); + } + + /** + * Loads the ui elements + */ + public void fillInnerParts() { + logger.info("loading ui elements"); + upcomingTasksPanel = UpcomingTasksPanel.load(primaryStage, getUpcomingTasksPlaceHolder(), + logic.getFilteredTaskList(), new UpcomingTasksPanel()); + completedTasksPanel = CompletedTasksPanel.load(primaryStage, getCompletedTasksPlaceHolder(), + logic.getFilteredTaskList(), new CompletedTasksPanel()); + floatingTasksPanel = FloatingTasksPanel.load(primaryStage, getFloatingTasksPlaceHolder(), + logic.getFilteredTaskList(), new FloatingTasksPanel()); + resultPopUp = ResultPopUp.load(primaryStage); + statusBarFooter = StatusBarFooter.load(primaryStage, getStatusbarPlaceholder(), config.getToDoListFilePath()); + commandBox = CommandBox.load(primaryStage, getCommandBoxPlaceholder(), messagePlaceHolder, resultPopUp, logic); + } + + public AnchorPane getCommandBoxPlaceholder() { + return commandBoxPlaceholder; + } + + public StackPane getMessagePlaceHolder() { + return messagePlaceHolder; + } + + public AnchorPane getStatusbarPlaceholder() { + return statusbarPlaceholder; + } + + public AnchorPane getUpcomingTasksPlaceHolder() { + return upcomingTasksPlaceHolder; + } + + public AnchorPane getCompletedTasksPlaceHolder() { + return completedTasksPlaceHolder; + } + + public AnchorPane getFloatingTasksPlaceHolder() { + return floatingTasksPlaceHolder; + } + + public UpcomingTasksPanel getUpcomingTasksPanel() { + return (UpcomingTasksPanel) this.upcomingTasksPanel; + } + + public CompletedTasksPanel getCompletedTasksPanel() { + return (CompletedTasksPanel) this.completedTasksPanel; + } + + public FloatingTasksPanel getFloatingasksPanel() { + return (FloatingTasksPanel) this.floatingTasksPanel; + + } + + //@@author + /** + * Sets the default size based on user preferences. + */ + private void setWindowDefaultSize(UserPrefs prefs) { + primaryStage.setHeight(prefs.getGuiSettings().getWindowHeight()); + primaryStage.setWidth(prefs.getGuiSettings().getWindowWidth()); + + if (prefs.getGuiSettings().getWindowCoordinates() != null) { + primaryStage.setX(prefs.getGuiSettings().getWindowCoordinates().getX()); + primaryStage.setY(prefs.getGuiSettings().getWindowCoordinates().getY()); + } + } + + private void setTitle(String appTitle) { + primaryStage.setTitle(appTitle); + } + + /** + * Returns the current size and the position of the main Window. + */ + public GuiSettings getCurrentGuiSetting() { + return new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), + (int) primaryStage.getX(), (int) primaryStage.getY()); + } + + //@@author A0148031R + @FXML + public void handleHelp() { + if(!messagePlaceHolder.getChildren().contains(helpWindow)) { + openHelpWindow(); + } + + } + + public void openHelpWindow() { + HelpWindow helpWindow = HelpWindow.load(primaryStage, messagePlaceHolder); + this.helpWindow = helpWindow.getMainPane(); + helpWindow.show(upcomingTasksPlaceHolder.getHeight()); + rootLayout.getChildren().remove(rootLayout.getChildren().indexOf(splitPane)); + } + + public void closeHelpWindow() { + messagePlaceHolder.getChildren().clear(); + messagePlaceHolder.setMaxHeight(0); + messagePlaceHolder.setPadding(new Insets(0)); + if(!rootLayout.getChildren().contains(splitPane)) { + rootLayout.getChildren().add(rootLayout.getChildren().indexOf(statusbarPlaceholder), splitPane); + } + } + + public void hide() { + primaryStage.hide(); + } + + public void show() { + primaryStage.show(); + } + + /** + * Closes the application. + */ + @FXML + private void handleExit() { + raise(new ExitAppRequestEvent()); + } + +} diff --git a/src/main/java/seedu/agendum/ui/ResultPopUp.java b/src/main/java/seedu/agendum/ui/ResultPopUp.java new file mode 100644 index 000000000000..25774cfb00ef --- /dev/null +++ b/src/main/java/seedu/agendum/ui/ResultPopUp.java @@ -0,0 +1,100 @@ +package seedu.agendum.ui; + +import javafx.animation.PauseTransition; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; +import seedu.agendum.commons.core.LogsCenter; + +import java.awt.Dimension; +import java.awt.Toolkit; +import java.util.logging.Logger; + +//@@author A0148031R +/** + * Controller for a pop up window that shows command execution result + */ +public class ResultPopUp extends UiPart { + private static final Logger logger = LogsCenter.getLogger(ResultPopUp.class); + private static final String FXML = "ResultPopUp.fxml"; + private static Stage root; + private AnchorPane mainPane; + private Stage dialogStage; + + private final PauseTransition delay = new PauseTransition(Duration.seconds(5)); + + @FXML + private Label resultDisplay; + + public static ResultPopUp load(Stage primaryStage) { + logger.fine("Showing command execution result."); + root = primaryStage; + ResultPopUp resultPopUp = UiPartLoader.loadUiPart(primaryStage, new ResultPopUp()); + resultPopUp.configure(); + return resultPopUp; + } + + @Override + public void setNode(Node node) { + mainPane = (AnchorPane) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + private void configure() { + + Scene scene = new Scene(mainPane); + + dialogStage = createDialogStage(null, null, scene); + dialogStage.initModality(Modality.NONE); + dialogStage.setAlwaysOnTop(true); + dialogStage.setOnShown((e1) -> primaryStage.requestFocus()); + + scene.setFill(Color.TRANSPARENT); + dialogStage.initStyle(StageStyle.TRANSPARENT); + + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + dialogStage.setMaxHeight(screenSize.getWidth()); + dialogStage.setMaxWidth(screenSize.getHeight()); + } + + private boolean isShowingMessage() { + return dialogStage.isShowing() && dialogStage.getOpacity() != 0; + } + + /** + * Shows message in a pop up window for several seconds + * @param message The command execution result to be shown + */ + public void postMessage(String message) { + + if(this.isShowingMessage()) { + delay.playFromStart(); + } else { + delay.setOnFinished(event -> dialogStage.setOpacity(0)); + delay.play(); + } + + resultDisplay.setWrapText(true); + resultDisplay.setText(message); + show(); + } + + private void show() { + dialogStage.setOpacity(1.0); + dialogStage.sizeToScene(); + dialogStage.show(); + dialogStage.setX(root.getX() + root.getWidth() / 2 - dialogStage.getWidth() / 2); + dialogStage.setY(root.getY() + root.getHeight() / 2 - dialogStage.getHeight() / 2); + } +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/agendum/ui/StatusBarFooter.java similarity index 51% rename from src/main/java/seedu/address/ui/StatusBarFooter.java rename to src/main/java/seedu/agendum/ui/StatusBarFooter.java index f74f66be6fc9..73879739f06f 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/agendum/ui/StatusBarFooter.java @@ -1,16 +1,28 @@ -package seedu.address.ui; +package seedu.agendum.ui; import com.google.common.eventbus.Subscribe; + +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.fxml.FXML; +import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; +import javafx.scene.paint.Color; import javafx.stage.Stage; +import javafx.util.Duration; + import org.controlsfx.control.StatusBar; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.util.FxViewUtil; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.events.model.ChangeSaveLocationEvent; +import seedu.agendum.commons.events.model.ToDoListChangedEvent; +import seedu.agendum.commons.util.FxViewUtil; +import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.logging.Logger; @@ -19,6 +31,7 @@ */ public class StatusBarFooter extends UiPart { private static final Logger logger = LogsCenter.getLogger(StatusBarFooter.class); + private static final String NOT_UPDATED_STATUS = "Not updated yet in this session"; private StatusBar syncStatus; private StatusBar saveLocationStatus; @@ -29,6 +42,9 @@ public class StatusBarFooter extends UiPart { @FXML private AnchorPane syncStatusBarPane; + + @FXML + private AnchorPane timeStatusBarPane; private AnchorPane placeHolder; @@ -40,12 +56,13 @@ public static StatusBarFooter load(Stage stage, AnchorPane placeHolder, String s return statusBarFooter; } - public void configure(String saveLocation) { + private void configure(String saveLocation) { addMainPane(); addSyncStatus(); - setSyncStatus("Not updated yet in this session"); + setSyncStatus(NOT_UPDATED_STATUS); addSaveLocation(); - setSaveLocation("./" + saveLocation); + setSaveLocation(saveLocation); + addTimeStatus(); registerAsAnEventHandler(this); } @@ -73,7 +90,15 @@ private void addSyncStatus() { FxViewUtil.applyAnchorBoundaryParameters(syncStatus, 0.0, 0.0, 0.0, 0.0); syncStatusBarPane.getChildren().add(syncStatus); } - + + //@@author A0148031R + private void addTimeStatus() { + Label timeStatus = new DigitalClock(); + FxViewUtil.applyAnchorBoundaryParameters(timeStatus, 0.0, 0.0, 0.0, 0.0); + timeStatus.setAlignment(Pos.CENTER); + timeStatusBarPane.getChildren().add(timeStatus); + } + @Override public void setNode(Node node) { mainPane = (GridPane) node; @@ -90,9 +115,39 @@ public String getFxmlPath() { } @Subscribe - public void handleAddressBookChangedEvent(AddressBookChangedEvent abce) { + public void handleToDoListChangedEvent(ToDoListChangedEvent event) { String lastUpdated = (new Date()).toString(); - logger.info(LogsCenter.getEventHandlingLogMessage(abce, "Setting last updated status to " + lastUpdated)); + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Setting last updated status to " + lastUpdated)); setSyncStatus("Last Updated: " + lastUpdated); } + + //@@author A0148095X + @Subscribe + public void handleChangeSaveLocationEvent(ChangeSaveLocationEvent event) { + String saveLocation = event.location; + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Setting save location to: " + saveLocation)); + setSaveLocation(saveLocation); + } } + +// @@author A0148031R +class DigitalClock extends Label { + + private static final String DATE_TIME_PATTERN = "HH:mm, EEE d MMM yyyy"; + + public DigitalClock() { + bindToTime(); + } + + private void bindToTime() { + Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(0), actionEvent -> { + Calendar time = Calendar.getInstance(); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_TIME_PATTERN); + setText(simpleDateFormat.format(time.getTime())); + setTextFill(Color.web("#ffffff")); + }), new KeyFrame(Duration.seconds(1))); + + timeline.setCycleCount(Animation.INDEFINITE); + timeline.play(); + } +} \ No newline at end of file diff --git a/src/main/java/seedu/agendum/ui/TaskCard.java b/src/main/java/seedu/agendum/ui/TaskCard.java new file mode 100644 index 000000000000..df7f8ebd59d2 --- /dev/null +++ b/src/main/java/seedu/agendum/ui/TaskCard.java @@ -0,0 +1,157 @@ +package seedu.agendum.ui; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontPosture; +import seedu.agendum.model.task.ReadOnlyTask; + +//@@author A0148031R +public class TaskCard extends UiPart { + + private static final String FXML = "TaskCard.fxml"; + private static final String OVERDUE_PREFIX = "Overdue\n"; + private static final String COMPLETED_PREFIX = "Completed on "; + private static final String TASK_TIME_PATTERN = "HH:mm EEE, dd MMM"; + private static final String COMPLETED_TIME_PATTERN = "EEE, dd MMM"; + private static final String START_TIME_PREFIX = "from "; + private static final String END_TIME_PREFIX = " to "; + private static final String DEADLINE_PREFIX = "by "; + private static final String EMPTY_PREFIX = ""; + private static final String OVERDUE_STYLE = "-fx-background-color: rgba(244, 67, 54, 0.8)"; + private static final String UPCOMING_STYLE = "-fx-background-color: rgba(255, 235, 59, 0.8)"; + private static final String OTHER_STYLE = "-fx-background-color: rgba(255,255,255,0.6)"; + private static final Color NAME_COLOR_DARK = Color.web("#3a3d42"); + private static final Color TIME_COLOR_DARK = Color.web("#4172c1"); + private static final Color NAME_COLOR_LIGHT = Color.web("#ffffff"); + private static final Color TIME_COLOR_LIGHT = Color.web("#fff59d"); + + @FXML + private HBox cardPane; + @FXML + private VBox taskVbox; + @FXML + private Label name; + @FXML + private Label id; + + private ReadOnlyTask task; + private String displayedIndex; + + public TaskCard() { + } + + public static TaskCard load(ReadOnlyTask task, int Index) { + TaskCard card = new TaskCard(); + card.task = task; + card.displayedIndex = String.valueOf(Index) + "."; + return UiPartLoader.loadUiPart(card); + } + + @FXML + public void initialize() { + + Label time = new Label(); + time.setId("time"); + + if (task.isOverdue()) { + cardPane.setStyle(OVERDUE_STYLE); + name.setTextFill(NAME_COLOR_LIGHT); + time.setTextFill(TIME_COLOR_LIGHT); + id.setTextFill(NAME_COLOR_LIGHT); + } else if (task.isUpcoming()) { + cardPane.setStyle(UPCOMING_STYLE); + name.setTextFill(NAME_COLOR_DARK); + time.setTextFill(TIME_COLOR_DARK); + } else { + cardPane.setStyle(OTHER_STYLE); + name.setTextFill(NAME_COLOR_DARK); + time.setTextFill(TIME_COLOR_DARK); + } + + StringBuilder timeDescription = new StringBuilder(); + timeDescription.append(formatTaskTime(task)); + + if (task.isCompleted()) { + timeDescription.append(formatUpdatedTime(task)); + } + + name.setText(task.getName().fullName); + id.setText(displayedIndex); + time.setText(timeDescription.toString()); + time.setMaxHeight(Control.USE_COMPUTED_SIZE); + time.setWrapText(true); + + if (task.hasTime() || task.isCompleted()) { + taskVbox.getChildren().add(time); + taskVbox.setAlignment(Pos.CENTER_LEFT); + time.setAlignment(Pos.CENTER_LEFT); + time.setFont(Font.font("Verdana", FontPosture.ITALIC, 11)); + } + } + + private String formatTime(String dateTimePattern, String prefix, Optional dateTime) { + + StringBuilder sb = new StringBuilder(); + DateTimeFormatter format = DateTimeFormatter.ofPattern(dateTimePattern); + sb.append(prefix).append(dateTime.get().format(format)); + + return sb.toString(); + } + + private String formatTaskTime(ReadOnlyTask task) { + + StringBuilder timeStringBuilder = new StringBuilder(); + + if (task.isOverdue()) { + timeStringBuilder.append(OVERDUE_PREFIX); + } + + if (task.isEvent()) { + String startTime = formatTime(TASK_TIME_PATTERN, START_TIME_PREFIX, task.getStartDateTime()); + String endTime = formatTime(TASK_TIME_PATTERN, END_TIME_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(startTime); + timeStringBuilder.append(endTime); + } else if (task.hasDeadline()) { + String deadline = formatTime(TASK_TIME_PATTERN, DEADLINE_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(deadline); + } + + return timeStringBuilder.toString(); + } + + private String formatUpdatedTime(ReadOnlyTask task) { + StringBuilder timeStringBuilder = new StringBuilder(); + if (task.hasTime()) { + timeStringBuilder.append("\n"); + } + timeStringBuilder.append(COMPLETED_PREFIX); + timeStringBuilder.append(formatTime(COMPLETED_TIME_PATTERN, EMPTY_PREFIX, + Optional.ofNullable(task.getLastUpdatedTime()))); + return timeStringBuilder.toString(); + } + + public HBox getLayout() { + return cardPane; + } + + @Override + public void setNode(Node node) { + cardPane = (HBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } +} diff --git a/src/main/java/seedu/agendum/ui/TasksPanel.java b/src/main/java/seedu/agendum/ui/TasksPanel.java new file mode 100644 index 000000000000..e4f9c9f8e0c0 --- /dev/null +++ b/src/main/java/seedu/agendum/ui/TasksPanel.java @@ -0,0 +1,54 @@ +package seedu.agendum.ui; + +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.control.SplitPane; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; + +//@@author A0148031R +/** + * Panel that contains the list of tasks + */ +public abstract class TasksPanel extends UiPart{ + private AnchorPane panel; + private AnchorPane placeHolderPane; + + public TasksPanel() { + super(); + } + + @Override + public void setNode(Node node) { + panel = (AnchorPane) node; + } + + @Override + public void setPlaceholder(AnchorPane pane) { + this.placeHolderPane = pane; + } + + public static TasksPanel load(Stage primaryStage, AnchorPane tasksPlaceholder, + ObservableList taskList, TasksPanel tasksPanelType) { + TasksPanel tasksPanel = UiPartLoader.loadUiPart(primaryStage, tasksPlaceholder, tasksPanelType); + tasksPanel.configure(taskList); + return tasksPanel; + } + + private void configure(ObservableList taskList) { + setConnections(taskList); + addToPlaceholder(); + } + + private void addToPlaceholder() { + SplitPane.setResizableWithParent(placeHolderPane, false); + placeHolderPane.getChildren().add(panel); + } + + protected abstract void setConnections(ObservableList allTasks); + + public abstract void scrollTo(Task task, boolean hasMultipleTasks); + +} diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/agendum/ui/Ui.java similarity index 88% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/seedu/agendum/ui/Ui.java index e6a67fe8c027..b22695f30ec4 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/seedu/agendum/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.agendum.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/agendum/ui/UiManager.java similarity index 67% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/seedu/agendum/ui/UiManager.java index 4a4dba3a2f6e..4166ad0dfdde 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/agendum/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.agendum.ui; import com.google.common.eventbus.Subscribe; import javafx.application.Platform; @@ -6,17 +6,18 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.ComponentManager; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.storage.DataSavingExceptionEvent; -import seedu.address.commons.events.ui.JumpToListRequestEvent; -import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; -import seedu.address.commons.events.ui.ShowHelpRequestEvent; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.model.UserPrefs; +import seedu.agendum.MainApp; +import seedu.agendum.commons.core.ComponentManager; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.core.LogsCenter; +import seedu.agendum.commons.events.storage.DataLoadingExceptionEvent; +import seedu.agendum.commons.events.storage.DataSavingExceptionEvent; +import seedu.agendum.commons.events.ui.JumpToListRequestEvent; +import seedu.agendum.commons.events.ui.CloseHelpWindowRequestEvent; +import seedu.agendum.commons.events.ui.ShowHelpRequestEvent; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.logic.Logic; +import seedu.agendum.model.UserPrefs; import java.util.logging.Logger; @@ -25,7 +26,7 @@ */ public class UiManager extends ComponentManager implements Ui { private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/agendum_icon.png"; private Logic logic; private Config config; @@ -62,7 +63,6 @@ public void start(Stage primaryStage) { public void stop() { prefs.updateLastUsedGuiSetting(mainWindow.getCurrentGuiSetting()); mainWindow.hide(); - mainWindow.releaseResources(); } private void showFileOperationAlertAndWait(String description, String details, Throwable cause) { @@ -74,7 +74,7 @@ private Image getImage(String imagePath) { return new Image(MainApp.class.getResourceAsStream(imagePath)); } - void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { + private void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); } @@ -99,28 +99,42 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { //==================== Event Handling Code ================================================================= + //@@author A0148095X + @Subscribe + private void handleDataLoadingExceptionEvent(DataLoadingExceptionEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + showFileOperationAlertAndWait("Could not load data", "Could not load data from file", event.exception); + } + + //@@author @Subscribe private void handleDataSavingExceptionEvent(DataSavingExceptionEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); showFileOperationAlertAndWait("Could not save data", "Could not save data to file", event.exception); } + //author A0148031R @Subscribe private void handleShowHelpEvent(ShowHelpRequestEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); mainWindow.handleHelp(); } - + @Subscribe private void handleJumpToListRequestEvent(JumpToListRequestEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); - mainWindow.getPersonListPanel().scrollTo(event.targetIndex); + if(event.targetTask.isCompleted()) { + mainWindow.getCompletedTasksPanel().scrollTo(event.targetTask, event.hasMultipleTasks); + } else if(event.targetTask.hasTime()) { + mainWindow.getUpcomingTasksPanel().scrollTo(event.targetTask, event.hasMultipleTasks); + } else { + mainWindow.getFloatingasksPanel().scrollTo(event.targetTask, event.hasMultipleTasks); + } } - + @Subscribe - private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event){ + public void handleCloseHelpWindowRequest(CloseHelpWindowRequestEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); - mainWindow.loadPersonPage(event.getNewSelection()); + mainWindow.closeHelpWindow(); } - } diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/seedu/agendum/ui/UiPart.java similarity index 89% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/seedu/agendum/ui/UiPart.java index 0a4ceb33e9b7..f38c95f700ac 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/seedu/agendum/ui/UiPart.java @@ -1,13 +1,14 @@ -package seedu.address.ui; +package seedu.agendum.ui; import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.image.Image; import javafx.scene.layout.AnchorPane; import javafx.stage.Modality; import javafx.stage.Stage; -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.events.BaseEvent; -import seedu.address.commons.util.AppUtil; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.events.BaseEvent; +import seedu.agendum.commons.util.AppUtil; /** * Base class for UI parts. @@ -18,11 +19,7 @@ public abstract class UiPart { /** * The primary stage for the UI Part. */ - Stage primaryStage; - - public UiPart(){ - - } + protected Stage primaryStage; /** * Raises the event via {@link EventsCenter#post(BaseEvent)} @@ -78,7 +75,7 @@ protected Stage createDialogStage(String title, Stage parentStage, Scene scene) * @param iconSource e.g. {@code "/images/help_icon.png"} */ protected void setIcon(String iconSource) { - primaryStage.getIcons().add(AppUtil.getImage(iconSource)); + primaryStage.getIcons().add(new Image(this.getClass().getResourceAsStream(iconSource))); } /** diff --git a/src/main/java/seedu/address/ui/UiPartLoader.java b/src/main/java/seedu/agendum/ui/UiPartLoader.java similarity index 91% rename from src/main/java/seedu/address/ui/UiPartLoader.java rename to src/main/java/seedu/agendum/ui/UiPartLoader.java index f880685a5b15..41d3261e87b4 100644 --- a/src/main/java/seedu/address/ui/UiPartLoader.java +++ b/src/main/java/seedu/agendum/ui/UiPartLoader.java @@ -1,10 +1,10 @@ -package seedu.address.ui; +package seedu.agendum.ui; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; -import seedu.address.MainApp; +import seedu.agendum.MainApp; /** * A utility class to load UiParts from FXML files. @@ -55,8 +55,8 @@ private static Node loadLoader(FXMLLoader loader, String fxmlFileName) { try { return loader.load(); } catch (Exception e) { - String errorMessage = "FXML Load Error for " + fxmlFileName; - throw new RuntimeException(errorMessage, e); + e.printStackTrace(); + return null; } } diff --git a/src/main/java/seedu/agendum/ui/UpcomingTasksPanel.java b/src/main/java/seedu/agendum/ui/UpcomingTasksPanel.java new file mode 100644 index 000000000000..d383d8dc1eb6 --- /dev/null +++ b/src/main/java/seedu/agendum/ui/UpcomingTasksPanel.java @@ -0,0 +1,95 @@ +package seedu.agendum.ui; + + +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.event.Event; +import javafx.fxml.FXML; +import javafx.scene.control.Control; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.MultipleSelectionModel; +import javafx.scene.control.SelectionMode; +import javafx.scene.input.MouseEvent; +import javafx.util.Duration; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; + +//@@author A0148031R +/** + * Panel contains the list of all uncompleted tasks with time + */ +public class UpcomingTasksPanel extends TasksPanel { + private static final String FXML = "UpcomingTasksPanel.fxml"; + private static ObservableList mainTaskList; + private MultipleSelectionModel selectionModel; + + @FXML + private ListView upcomingTasksListView; + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + protected void setConnections(ObservableList taskList) { + mainTaskList = taskList; + upcomingTasksListView.setItems(taskList.filtered(task -> task.hasTime() && !task.isCompleted())); + upcomingTasksListView.setCellFactory(listView -> new upcomingTasksListViewCell()); + configure(); + } + + private void configure() { + selectionModel = upcomingTasksListView.getSelectionModel(); + upcomingTasksListView.setSelectionModel(null); + upcomingTasksListView.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume); + } + + /** + * Scrolls to the newly updated task and highlight for several seconds. If + * there are multiple tasks updated, previous highlight will not be cleared. + */ + public void scrollTo(Task task, boolean hasMultipleTasks) { + Platform.runLater(() -> { + + int index = mainTaskList.indexOf(task); + upcomingTasksListView.scrollTo(index); + upcomingTasksListView.setSelectionModel(selectionModel); + + if(hasMultipleTasks) { + upcomingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + upcomingTasksListView.getSelectionModel().select(index); + } else { + upcomingTasksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + upcomingTasksListView.getSelectionModel().clearAndSelect(index); + } + + PauseTransition delay = new PauseTransition(Duration.seconds(5)); + delay.setOnFinished(event -> upcomingTasksListView.getSelectionModel().clearSelection(index)); + delay.play(); + }); + } + + class upcomingTasksListViewCell extends ListCell { + + public upcomingTasksListViewCell() { + prefWidthProperty().bind(upcomingTasksListView.widthProperty()); + setMaxWidth(Control.USE_PREF_SIZE); + } + + @Override + protected void updateItem(ReadOnlyTask task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(TaskCard.load(task, mainTaskList.indexOf(task) + 1).getLayout()); + } + } + } + +} diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd93..000000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/agendum_icon.png b/src/main/resources/images/agendum_icon.png new file mode 100644 index 000000000000..314038585819 Binary files /dev/null and b/src/main/resources/images/agendum_icon.png differ diff --git a/src/main/resources/images/calendar.png b/src/main/resources/images/calendar.png deleted file mode 100644 index 8b2bdf4f1c16..000000000000 Binary files a/src/main/resources/images/calendar.png and /dev/null differ diff --git a/src/main/resources/images/clock.png b/src/main/resources/images/clock.png deleted file mode 100644 index 0807cbf64519..000000000000 Binary files a/src/main/resources/images/clock.png and /dev/null differ diff --git a/src/main/resources/images/completedTasksPanel.png b/src/main/resources/images/completedTasksPanel.png new file mode 100644 index 000000000000..a2daa979517d Binary files /dev/null and b/src/main/resources/images/completedTasksPanel.png differ diff --git a/src/main/resources/images/fail.png b/src/main/resources/images/fail.png deleted file mode 100644 index 6daf01290dde..000000000000 Binary files a/src/main/resources/images/fail.png and /dev/null differ diff --git a/src/main/resources/images/floatingTasksPanel.png b/src/main/resources/images/floatingTasksPanel.png new file mode 100644 index 000000000000..19155b10c5bf Binary files /dev/null and b/src/main/resources/images/floatingTasksPanel.png differ diff --git a/src/main/resources/images/help_icon.png b/src/main/resources/images/help_icon.png deleted file mode 100644 index f8e80d6c1c51..000000000000 Binary files a/src/main/resources/images/help_icon.png and /dev/null differ diff --git a/src/main/resources/images/info_icon.png b/src/main/resources/images/info_icon.png deleted file mode 100644 index f8cef714095b..000000000000 Binary files a/src/main/resources/images/info_icon.png and /dev/null differ diff --git a/src/main/resources/images/upcomingTasksPanel.png b/src/main/resources/images/upcomingTasksPanel.png new file mode 100644 index 000000000000..184ba632f723 Binary files /dev/null and b/src/main/resources/images/upcomingTasksPanel.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 575de420b994..ce111eb0f87f 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -3,7 +3,7 @@ + fx:controller="seedu.agendum.ui.CommandBox" stylesheets="@DarkTheme.css"> diff --git a/src/main/resources/view/CompletedTasksPanel.fxml b/src/main/resources/view/CompletedTasksPanel.fxml new file mode 100644 index 000000000000..ac0c7203bcab --- /dev/null +++ b/src/main/resources/view/CompletedTasksPanel.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 8043b344253a..dbb4c5a66a3d 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,26 +1,6 @@ +/* @@author A0148031R */ .background { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.label { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; - -fx-opacity: 0.9; -} - -.label-bright { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; - -fx-opacity: 1; -} - -.label-header { - -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 1; + -fx-background-color: derive(#263238, 20%); } .text-field { @@ -38,104 +18,55 @@ -fx-max-height: 0; } -.table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; - -fx-table-cell-border-color: transparent; - -fx-table-header-border-color: transparent; - -fx-padding: 5; -} - -.table-view .column-header-background { - -fx-background-color: transparent; -} - -.table-view .column-header, .table-view .filler { - -fx-size: 35; - -fx-border-width: 0 0 1 0; - -fx-background-color: transparent; - -fx-border-color: - transparent - transparent - derive(-fx-base, 80%) - transparent; - -fx-border-insets: 0 10 1 0; -} - -.table-view .column-header .label { - -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-alignment: center-left; - -fx-opacity: 1; -} - -.table-view:focused .table-row-cell:filled:focused:selected { - -fx-background-color: -fx-focus-color; -} - .split-pane:horizontal .split-pane-divider { - -fx-border-color: transparent #1d1d1d transparent #1d1d1d; - -fx-background-color: transparent, derive(#1d1d1d, 10%); + -fx-border-color: transparent; + -fx-background-color: transparent; } .split-pane { -fx-border-radius: 1; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); -} - -.list-cell { - -fx-label-padding: 0 0 0 0; - -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; -} - -.list-cell .label { - -fx-text-fill: #010504; -} - -.cell_big_label { - -fx-font-size: 16px; - -fx-text-fill: #010504; -} - -.cell_small_label { - -fx-font-size: 11px; - -fx-text-fill: #010504; + -fx-background-color: derive(#263238, 20%); } .anchor-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#263238, 20%); } .anchor-pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); + -fx-background-color: derive(#263238, 20%); + -fx-border-color: transparent; + -fx-border-top-width: 1px; +} + +.stack-pane { + -fx-background-color: derive(#263238, 20%); + -fx-border-color: transparent; -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-text-fill: black; + -fx-background-color: derive(#263238, 20%); + -fx-border-color: transparent; + -fx-text-fill: white; } .result-display { -fx-background-color: #ffffff; } -.result-display .label { - -fx-text-fill: black !important; +.result-display { + -fx-text-fill: black; } .status-bar .label { + -fx-background-color: transparent; -fx-text-fill: white; } .status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#263238, 30%); + -fx-border-color: transparent; -fx-border-width: 1px; } @@ -144,17 +75,17 @@ } .grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#263238, 30%); + -fx-border-color: derive(#263238, 30%); -fx-border-width: 1px; } .grid-pane .anchor-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#263238, 20%); } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#263238, 50%); } .context-menu .label { @@ -162,7 +93,7 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#263238, 20%); } .menu-bar .label { @@ -186,7 +117,7 @@ -fx-border-color: #e2e2e2; -fx-border-width: 2; -fx-background-radius: 0; - -fx-background-color: #1d1d1d; + -fx-background-color: #263238; -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; -fx-font-size: 11pt; -fx-text-fill: #d8d8d8; @@ -199,7 +130,7 @@ .button:pressed, .button:default:hover:pressed { -fx-background-color: white; - -fx-text-fill: #1d1d1d; + -fx-text-fill: #263238; } .button:focused { @@ -212,7 +143,7 @@ .button:disabled, .button:default:disabled { -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; + -fx-background-color: #263238; -fx-text-fill: white; } @@ -226,11 +157,11 @@ } .dialog-pane { - -fx-background-color: #1d1d1d; + -fx-background-color: #263238; } .dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; + -fx-background-color: #263238; } .dialog-pane > *.label.content { @@ -240,7 +171,7 @@ } .dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#263238, 25%); } .dialog-pane:header *.header-panel *.label { @@ -250,32 +181,26 @@ -fx-text-fill: white; } -.scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); - -fx-background-insets: 3; -} - -.scroll-bar .increment-button, .scroll-bar .decrement-button { - -fx-background-color: transparent; - -fx-padding: 0 0 0 0; -} - -.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { - -fx-shape: " "; +.list-view .scroll-bar:vertical .increment-arrow, +.list-view .scroll-bar:vertical .decrement-arrow, +.list-view .scroll-bar:vertical .increment-button, +.list-view .scroll-bar:vertical .decrement-button { + -fx-padding:0; } -.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { - -fx-padding: 1 8 1 8; +.list-view .scroll-bar:horizontal .increment-arrow, +.list-view .scroll-bar:horizontal .decrement-arrow, +.list-view .scroll-bar:horizontal .increment-button, +.list-view .scroll-bar:horizontal .decrement-button { + -fx-padding:0; } -.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { - -fx-padding: 8 1 8 1; +.list-view { + -fx-background-radius: 10; } #cardPane { -fx-background-color: transparent; - -fx-border-color: #d6d6d6; - -fx-border-width: 1 1 1 1; } #commandTypeLabel { @@ -283,6 +208,6 @@ -fx-text-fill: #F70D1A; } -#filterField, #personListPanel, #personWebpage { +#filterField, #taskListPanel, #taskWebpage { -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } \ No newline at end of file diff --git a/src/main/resources/view/DefaultBrowserPlaceHolderScreen.fxml b/src/main/resources/view/DefaultBrowserPlaceHolderScreen.fxml deleted file mode 100644 index bc761118235a..000000000000 --- a/src/main/resources/view/DefaultBrowserPlaceHolderScreen.fxml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index 606c927d9a42..2890622f6aaa 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -6,7 +6,7 @@ .tag-selector { -fx-border-width: 1; - -fx-border-color: white; + -fx-border-color: transparent; -fx-border-radius: 3; -fx-background-radius: 3; } diff --git a/src/main/resources/view/FloatingTasksPanel.fxml b/src/main/resources/view/FloatingTasksPanel.fxml new file mode 100644 index 000000000000..7ac9a40bd5a3 --- /dev/null +++ b/src/main/resources/view/FloatingTasksPanel.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css new file mode 100644 index 000000000000..b7c58e72b0c8 --- /dev/null +++ b/src/main/resources/view/HelpWindow.css @@ -0,0 +1,105 @@ +/* @@author A0148031R */ + +.pane { + -fx-background-color: #EA6254; + -fx-background-radius: 10; +} + +.button { + -fx-background-color: rgba(0,0,0,0.1); + -fx-background-radius: 10; +} + +.button:hover { + -fx-background-color: rgba(0,0,0,0.3); + -fx-background-radius: 10; +} + +.table-view:focused{ + -fx-background-color: transparent; +} + +.table-view{ + -fx-background-color: transparent; +} + +.table-view .column-header-background{ + -fx-background-color: transparent; +} + +.table-view .column-header-background .label{ + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-font-size: 16pt; +} + +.table-view .column-header { + -fx-background-color: transparent; +} + +.table-view .table-cell{ + -fx-text-fill: white; + -fx-font-size: 14pt; +} + +.table-cell:empty { + -fx-background-color: transparent; +} + +.table-row-cell{ + -fx-table-cell-border-color: transparent; + -fx-background-color: transparent; +} + +.table-row-cell:even{ + -fx-table-cell-border-color: transparent; + -fx-background-color: rgba(255,255,255,0.3); +} + +.table-row-cell:hover{ + -fx-background-color: rgba(0,0,0,0.1); +} + +.scroll-bar .increment-button { + -fx-opacity: 0; +} + +.scroll-bar .decrement-button { + -fx-opacity: 0; +} + +.scroll-bar { + -fx-background-color: transparent; +} + +.scroll-bar .track-background { + -fx-opacity: 0; + -fx-background-color: transparent; + -fx-background-insets: 0; +} + +.scroll-bar .track { + -fx-opacity: 0; + -fx-background-color: transparent; + -fx-border-color:transparent; +} + +.scroll-bar .thumb { + -fx-background-color: transparent; + -fx-background-insets: 4 0 4 0; + -fx-background-radius: 2em; +} + +.table-view .scroll-bar:vertical .increment-arrow, +.table-view .scroll-bar:vertical .decrement-arrow, +.table-view .scroll-bar:vertical .increment-button, +.table-view .scroll-bar:vertical .decrement-button { + -fx-padding:0; +} + +.table-view .scroll-bar:horizontal .increment-arrow, +.table-view .scroll-bar:horizontal .decrement-arrow, +.table-view .scroll-bar:horizontal .increment-button, +.table-view .scroll-bar:horizontal .decrement-button { + -fx-padding:0; +} \ No newline at end of file diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index c4cbd84cac28..eb87c1502d6f 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -1,8 +1,41 @@ + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 2f9235c621d8..a1a34c21ab9d 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -1,10 +1,18 @@ - - - - + + + + + + + + + + + + @@ -19,36 +27,47 @@ - + - + - + - - - - - - + + + + + + - + - + - + - - - - - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml deleted file mode 100644 index 13d4b149651b..000000000000 --- a/src/main/resources/view/PersonListCard.fxml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml deleted file mode 100644 index 000c4c999b65..000000000000 --- a/src/main/resources/view/PersonListPanel.fxml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml deleted file mode 100644 index cc650d739e22..000000000000 --- a/src/main/resources/view/ResultDisplay.fxml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/src/main/resources/view/ResultPopUp.css b/src/main/resources/view/ResultPopUp.css new file mode 100644 index 000000000000..fb93f0429ebe --- /dev/null +++ b/src/main/resources/view/ResultPopUp.css @@ -0,0 +1,11 @@ +/*@@author A0148031R */ +.pane { + -fx-background-color: rgb(229,74,63); + -fx-background-radius: 20; + -fx-padding: 10px; +} + +.label { + -fx-text-fill: white; + -fx-font-size: 14pt; +} \ No newline at end of file diff --git a/src/main/resources/view/ResultPopUp.fxml b/src/main/resources/view/ResultPopUp.fxml new file mode 100644 index 000000000000..cca69919bfc1 --- /dev/null +++ b/src/main/resources/view/ResultPopUp.fxml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/main/resources/view/StatusBarFooter.fxml b/src/main/resources/view/StatusBarFooter.fxml index 2656558b6eb7..90a1636ac198 100644 --- a/src/main/resources/view/StatusBarFooter.fxml +++ b/src/main/resources/view/StatusBarFooter.fxml @@ -1,13 +1,23 @@ - - + + + + + + + + - - + + + + + + diff --git a/src/main/resources/view/TaskCard.fxml b/src/main/resources/view/TaskCard.fxml new file mode 100644 index 000000000000..34d6ddc4da63 --- /dev/null +++ b/src/main/resources/view/TaskCard.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/TasksPanel.css b/src/main/resources/view/TasksPanel.css new file mode 100644 index 000000000000..f36240244f6b --- /dev/null +++ b/src/main/resources/view/TasksPanel.css @@ -0,0 +1,52 @@ +/* @@author A0148031R */ +.all-pane { + -fx-background-color: #4DB6AC; + -fx-background-radius: 10; +} + +.completed-pane { + -fx-background-color: #727a87; + -fx-background-radius: 10; +} + +.other-pane { + -fx-background-color: #3498DB; + -fx-background-radius: 10; +} + +.list-view { + -fx-background-color: transparent; + -fx-background-radius: 10; +} + +.list-cell { + -fx-background-color: transparent; + -fx-background-radius: 10; +} + +.list-cell:empty { + -fx-background-color: transparent; +} + +.list-cell:filled:selected:focused, .list-cell:filled:selected { + -fx-background-color: #3949AB; + -fx-text-fill: red; +} + +.cell_big_label { + -fx-font-size: 14pt; + -fx-opacity: 0.9; +} + +.cell_small_label { + -fx-text-fill: #3a3d42; + -fx-opacity: 0.9; +} + +.hbox { + -fx-background-radius: 10; +} + +.vbox { + -fx-background-color: transparent; +} \ No newline at end of file diff --git a/src/main/resources/view/UpcomingTasksPanel.fxml b/src/main/resources/view/UpcomingTasksPanel.fxml new file mode 100644 index 000000000000..cfd8a42c76ed --- /dev/null +++ b/src/main/resources/view/UpcomingTasksPanel.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/data/ConfigUtilTest/ExtraValuesConfig.json b/src/test/data/ConfigUtilTest/ExtraValuesConfig.json index 578b4445204b..cc41b2970724 100644 --- a/src/test/data/ConfigUtilTest/ExtraValuesConfig.json +++ b/src/test/data/ConfigUtilTest/ExtraValuesConfig.json @@ -2,7 +2,7 @@ "appTitle" : "Typical App Title", "logLevel" : "INFO", "userPrefsFilePath" : "C:\\preferences.json", - "addressBookFilePath" : "addressbook.xml", - "addressBookName" : "TypicalAddressBookName", + "toDoListFilePath" : "todolist.xml", + "toDoListName" : "TypicalToDoListName", "extra" : "extra value" } \ No newline at end of file diff --git a/src/test/data/ConfigUtilTest/TypicalConfig.json b/src/test/data/ConfigUtilTest/TypicalConfig.json index 195b2bf33033..acc2113d195d 100644 --- a/src/test/data/ConfigUtilTest/TypicalConfig.json +++ b/src/test/data/ConfigUtilTest/TypicalConfig.json @@ -2,6 +2,6 @@ "appTitle" : "Typical App Title", "logLevel" : "INFO", "userPrefsFilePath" : "C:\\preferences.json", - "addressBookFilePath" : "addressbook.xml", - "addressBookName" : "TypicalAddressBookName" + "toDoListFilePath" : "todolist.xml", + "toDoListName" : "TypicalToDoListName" } \ No newline at end of file diff --git a/src/test/data/ManualTesting/SampleData.xml b/src/test/data/ManualTesting/SampleData.xml new file mode 100644 index 000000000000..f14d74f6d367 --- /dev/null +++ b/src/test/data/ManualTesting/SampleData.xml @@ -0,0 +1,301 @@ + + + + Submit CS2103 Assignment + false + 2016-11-06 10:27:30 + 2016-10-26 20:00:00 + + + Write essay for IS1103 + false + 2016-11-06 10:27:30 + 2016-11-07 23:59:00 + + + CS2103 group meeting + false + 2016-11-06 10:27:30 + 2016-11-04 12:00:00 + 2016-11-04 14:00:00 + + + CS2103 group meeting + false + 2016-11-06 10:31:26 + 2016-11-11 12:00:00 + 2016-11-11 14:00:00 + + + Clear wardrobe clutter + false + 2016-11-06 10:27:30 + 2016-12-31 23:59:00 + + + Buy soap and shampoo + false + 2016-10-30 12:49:30 + + + Deposit $50 into bank + false + 2016-10-29 14:53:10 + + + Buy new headphones + false + 2016-10-29 14:52:52 + + + Proofread CS2103 reflection + false + 2016-11-06 10:27:30 + 2016-10-29 20:00:00 + 2016-10-29 22:00:00 + + + Clean dust inside PC + true + 2016-11-06 10:27:30 + + + Buy new i7-6700k CPU + false + 2016-10-29 14:52:08 + + + Search for Shaun Mendez songs + true + 2016-11-06 10:27:30 + + + Write reflection for CS2103 + true + 2016-11-06 10:28:17 + 2016-10-26 00:00:00 + + + Read up on Extremism + false + 2016-10-29 14:55:22 + + + Read 'The Mythical Man-Month' + false + 2016-10-29 14:58:08 + + + Decorate house + false + 2016-11-06 10:29:43 + 2016-12-25 00:00:00 + + + Buy present for Jim + false + 2016-11-06 10:27:30 + 2016-11-04 23:59:00 + + + CS2101 Oral Presentation 1 + true + 2016-11-06 10:27:30 + 2016-09-06 12:00:00 + 2016-09-06 13:00:00 + + + CS2103 Post-Lecture Quiz + false + 2016-11-06 10:27:30 + 2016-11-05 12:50:11 + + + Complete peer review on TEAMMATES + false + 2016-11-06 10:27:30 + 2016-10-10 12:00:00 + + + Go to NTUC + false + 2016-11-03 17:14:50 + + + Attend CS2103 lecture + true + 2016-11-06 10:27:30 + 2016-11-04 14:00:00 + 2016-11-04 16:00:00 + + + Visit relatives + true + 2016-11-06 10:27:30 + 2000-02-01 17:16:02 + 2000-02-05 17:16:02 + + + Revise for exams + false + 2016-11-06 10:27:30 + 2016-11-14 00:00:00 + 2016-11-19 00:00:00 + + + Download Pokemon GO + false + 2016-11-03 17:16:29 + + + Sister's wedding + false + 2016-11-06 10:27:30 + 2016-12-20 00:00:00 + 2016-12-21 00:00:00 + + + Visit grandma + false + 2016-11-06 10:27:30 + 2016-12-10 12:00:00 + 2016-12-10 17:00:00 + + + CS2103 V0.1 + true + 2016-11-06 10:27:30 + 2016-10-05 00:00:00 + + + CS2103 V0.2 + true + 2016-11-06 10:27:30 + 2016-10-12 00:00:00 + + + CS2103 V0.3 + true + 2016-11-06 10:27:30 + 2016-10-19 00:00:00 + + + CS2103 V0.4 + true + 2016-11-06 10:27:30 + 2016-10-26 00:00:00 + + + CS2103 V0.5rc + true + 2016-11-06 10:27:30 + 2016-11-02 00:00:00 + + + CS2103 V0.5 + false + 2016-11-06 10:27:30 + 2016-11-07 17:22:54 + + + Army training + false + 2016-11-06 10:27:30 + 2016-12-12 00:00:00 + 2016-12-16 00:00:00 + + + Go to CS2103 lecture + true + 2016-11-06 10:27:30 + 2016-10-28 14:00:00 + 2016-10-28 16:00:00 + + + CS2103 Final Exam + false + 2016-11-06 10:27:30 + 2016-11-26 13:00:00 + 2016-11-26 15:00:00 + + + Buy groceries for next week + false + 2016-11-06 10:27:30 + 2016-11-06 23:59:00 + + + Transfer money back to Jim + false + 2016-11-06 09:58:57 + + + Buy Nvidia GTX 1090 Ti + false + 2016-11-06 10:00:22 + + + Data Privacy Asia 2016 + false + 2016-11-06 10:27:30 + 2016-11-09 00:00:00 + 2016-11-09 23:59:00 + + + Cybersecurity Nexus Conference + false + 2016-11-06 10:27:30 + 2016-11-15 08:00:00 + 2016-11-15 12:00:00 + + + Buy tickets to singapore international film festival + false + 2016-11-06 10:07:42 + + + Social Media Marketing Course + false + 2016-11-06 10:27:30 + 2016-11-28 09:00:00 + 2016-11-28 18:00:00 + + + Binge watch Game of Thrones + false + 2016-11-06 10:29:13 + 2016-11-08 00:00:00 + 2016-11-08 10:00:00 + + + Sharpen pencils for exams + false + 2016-11-06 10:30:51 + 2016-11-13 23:59:00 + + + Try new ramen at Menya Musashi + false + 2016-11-06 10:31:57 + + + Watch Dr. Strange + false + 2016-11-06 10:32:17 + + + Buy holy oil from kong hee + false + 2016-11-06 10:32:48 + + + Look for salad dressing in NTUC + false + 2016-11-06 10:34:38 + + + Buy pimple cream + false + 2016-11-06 10:35:09 + + diff --git a/src/test/data/ManualTesting/testScript.md b/src/test/data/ManualTesting/testScript.md new file mode 100644 index 000000000000..41f7908ac175 --- /dev/null +++ b/src/test/data/ManualTesting/testScript.md @@ -0,0 +1,276 @@ +[comment]: # (@@author A0148031R) +# Test Script for Manual Testing + +## Loading the sample data + +1. Ensure that you put `SampleData.xml` inside the `/src/test/data/ManualTesting` folder. +2. Start `Agendum.jar` by double clicking it. +3. Type `load src/test/data/ManualTesting/SampleData.xml`. + +####Result: +* In the **Do It Soon** column, there should be 22 tasks appearing with indices from 1 to 22. Tasks highlighted in red have a start/end time before the current day and time; task highlighted in yellow have a start/end time within the current week and the remaining are not highlighted. +* In the **Do It Anytime** column, there should be 16 tasks appearing, with indices from 23 to 38. +* In the **Done** column, there should be 12 tasks appearing, with indices 39-50. + +## Help + +1. Type `help` +2. Press ESC +3. Press Ctrl H +4. Press Ctrl H + +####Result: +1. A help window appears in a table style, listing out all the command available, together with the command description and format. +2. Agendum exits the help window. +3. The help window will appear. +4. Agendum exits the help window. + +## Date Time Formats + +Before proceeding to add or schedule tasks with dates and time, please refer to the tables below. They describe the date format and time format Agendum supports. Combine any of the date format and time format below. The date/time formats are case insensitive too. + +*Date Format* + +| Date Format | Example(s) | +|-----------------|----------------------| +| Month/day | 1/23 | +| Day Month | 1 Oct | +| Month Day | Oct 1 | +| Day of the week | Wed, Wednesday | +| Relative date | today, tmr, next wed | + + > If no year is specified, it is always assumed to be the current year. + > It is possible to specify the year before or after the month-day pair in the first 3 formats (e.g. 1/23/2016 or 2016 1 Oct) + > The day of the week refers to the following week. For example, today is Sunday (30 Oct). Agendum will interpret Wednesday and Sunday as 2 Nov and 6 Nov respectively (a week from now). + +*Time Format* + +| Time Format | Example(s) | +|-----------------|-----------------------------------------| +| Hour | 10, 22 | +| Hour:Minute | 10:30 | +| Hour.Minute | 10.30 | +| Relative time | this morning, this afternoon, tonight | + +> By default, we use the 24-hour time format but we do support the meridian format as well e.g. 10am, 10pm + +Note +> If no year, date or time is specified, the current year, date or time will be used in its place. +> It is advisable to specify both the date and time. + +## Add +To add a task, you have to start your command with the keyword `add`. + +>Here are the *acceptable format(s)*: + +> * `add ` - adds a task which can be done anytime. +* `add by ` - adds a task which have to be done by the specified deadline. Note the keyword `by`. +* `add from to ` - adds a event which will take place between start time and end time. Note the keyword `from` and `to`. + + +### 1. Add a floating task +Type `add watch movie`. +#### Result +A new floating task named `watch movie` is created at the top of **Do It Anytime** column, and highlighted in purple borders. + +### 2. Add a task with deadline +Type `add submit essay by 10pm`. +#### Result +A new task named `submit essay` is created in the **Do It Soon** column, with its deadline under the name of this task. Also, it is highlighted in purple borders. + +### 3. Add a task with event time +Type `add go for church camp from 20 nov 2pm to 25 nov 5pm` +#### Result +A new task named `go for church` is created in the **Do It Soon** column, with its deadline under the name of this task . Also, it is highlighted in purple borders. + +### 4. Add a task with name enclosed in single quotation mark +Type `add 'drop by store' by tmr` +#### Result +A new task named `drop by store` is created in the **Do It Soon** column, with its deadline (tomorrow's date) under the name of this task. +>Sometimes Agendum may incorrectly interpret part of task name as a deadline/event time. To avoid this misinterpretation, you can add single quotation mark around the task name. Agendum will then intepret what is enclosed within the single quotation mark as the task name and will not attempt to parse it as time. + + +## Rename +To rename a task, you have to start your command with the keyword `rename`. + +>Here is the *acceptable format* + +> * `rename ` - give a new name to the task identified by . The must be a positive number and be in the most recent to-do list displayed. + +Type `rename 1 complete peer review on TEAMMATES for 2101`. Note the task original name as its id change after it has been renamed. +#### Result +The original task with id 1 is renamed to `complete peer review on TEAMMATES for 2101`. Also, it is highlighted in purple borders. + +## (Re)schedule +To reschedule a task, you have to start your command with the keyword `schedule`. + +>Here are the *acceptable format(s)*: + +>* `schedule ` - re-schedule the task identified by ``. It can now be done anytime. It is no longer bounded by a deadline or event time! +* `schedule by ` - set or update the deadline for the task identified. Note the keyword `by`. +* `schedule from to ` - update the start/end time of the task identified by ``. Note the keyword `from` and `to`. + +###1. Schedule a task with no deadline or event time +Type `schedule 22`. Note the task name as its id change after it has been rescheduled. Use the task name to identify the task. +####Result +The original task with id 22 now has no deadline, and it is shifted to **Do It Anytime** column with a new id. Also, it is highlighted in purple borders. + +###2. Schedule a task with a deadline +Type `schedule 23 by 11 nov 10pm`. Note the task name as its id change after it has been rescheduled. +####Result +The original task with id 23 now has a deadline, and it is shifted to **Do It Soon** column with a new id. Also, it is highlighted in purple borders. + +###3. Schedule a task with event time +Type `schedule 14 from 13 nov 2pm to 13 nov 4pm`. Note the task name as its id change after it has been rescheduled. +####Result +The original task with id 14 now has an event time, and it is assigned with a new id. Also, it is highlighted in purple borders. + +## Delete +To delete a task, you have to start your command with the keyword `delete`. + +>Here is the *format*: + +>* `delete ...` - Deletes the task at the specified `id`. Each `` must be a positive number and in the most recent to-do list displayed. If there are multiple ``s, they should be separated by commas or space (e.g. 1,2 or 1 2). You can also input a range (e.g. 3-5) + +###1. Delete a single task +Type `delete 10`. +####Result +Task with id 10 is deleted. + +###2. Delete multiple tasks +Type `delete 11 21-23` +####Result +Tasks with indices 11, 21, 22 and 23 are deleted. + +## Mark +To mark a task as completed, you have to start your command with the keyword `mark`. + +>Here is the *format*: + +>* `mark ...` - mark all the tasks identified by ``(s) as completed. Each `` must be a positive number and in the most recent to-do list displayed. If there are multiple ``s, they should be separated by commas or space (e.g. 1,2 or 1 2). You can also input a range (e.g. 3-5) + +###1. Mark a single task +Type `mark 1`. Note the task name as its id change after it has been marked. +####Result +The original task with id 1 is marked as completed, shifting from **Do It Soon** column to **Done** column, and the completion time is shown below the task name. Also, it is highlighted in purple borders. + +###2. Mark multiple tasks +Type `mark 1 3-5`. Note the task names as ids change after they have been marked. +####Result +Tasks with old indices 1, 3, 4 and 5 are marked as completed, shifting from **Do It Soon** column to **Done** column, and their completion times are shown below their task names. Also, they are all highlighted in purple borders. +(The operation will only be successful if no duplicate task will result) + +## Unmark +To unmark a task as completed, you have to start your command with the keyword `unmark`. + +>Here is the *format*: + +>* `unmark ...` - unmark all the tasks identified by ``(s) as uncompleted. Each `` must be a positive number and in the most recent to-do list displayed. If there are multiple ``s, they should be separated by commas or space (e.g. 1,2 or 1 2). You can also input a range (e.g. 3-5) + +###1. Unmark a single task +Type `unmark 36`. Note the task name as its id change after it has been unmarked. +####Result +The original task with id 36 are unmarked, shifting from **Done** column to **Do It Soon** column, and there is no more completion time under the task name. Also, it is highlighted in purple borders. If there are multiple ``s, they should be separated by commas, space, or input as a range. + +###2. Unmark multiple tasks +Type `unmark 37-40`. Note the task names as ids change after they have been unmarked. +####Result +Tasks with old indices 37, 38, 39 and 40 are unmarked, shifting from **Done** column to **Do It Soon** column, and there are no more completion times under their task names. Also, they are all highlighted in purple borders. +(The operation will only be successful if no duplicate task will result) + +## Undo +Undo is supposed to reverse the last changes to the task list (e.g. by `add`, `delete`, `rename`, `mark`, `unmark`, `schedule`). Multiple undo are supported. + +1. Type `undo`. +2. Press Ctrl Z + +####Result +1. The previously unmarked tasks (_from `unmark 37-40`_) are marked as completed again, and the affected tasks are highlighted in purple borders. +2. The previously unmarked task (_from `unmark 36`_) is marked as completed again, and the affected task is highlighted in purple borders. + +## Find +>Here is the *format*: + +>* `find ...` - filter out all tasks containing any of the keyword(s) given. The keywords should be in full word(s), and they are case insensitive. + +1. Type `find cs2103` +2. Type `find buy visit` +3. Press ESC + +####Result +1. After the first step, tasks containing `cs2103` in their names will be listed out. +2. After the second step, tasks containing `buy` or `visit` in their names will be listed out. +3. After the third step, agendum will exit the find results and go back to list all the tasks. + +## List +After you are done with searching for tasks, you can use `list` to exit your find results and see a list of tasks. Alternatively, you can press ESC to go exit. + +1. Type `list` +2. Type `find cs2103` +3. Press ESC + +####Result +1. After the first step, agendum will exit the find results and go back to list all the tasks. +2. After the second step, agendum will list out tasks containing `cs2103` in their names. +3. After the third step, agendum will exit the find results and go back to list all the tasks. + +## Alias +To alias a command, you have to start your command with the keyword `alias`. +>Here is the *format*: + +>* `alias ` - `` must be a single alphanumeric word, and it cannot be an original-command or already aliased to another command. `` must be a command word that is specified in the Command Summary section (or in the help window) + +1. Type `alias schedule s` +2. Type `s 1 by 10pm` + +####Result +1. After the first step, `s` is created as an alias command for `schedule`, and you can now use both `s` and `schedule` to reschedule a task. +2. After the second step, task with id 1 is rescheduled to be by today 10pm. + +## Unalias +To unalias a command, you have to start your command with the keyword `unalias`. +>Here is the *format*: + +>* `unalias ` - `` should be an alias you previously defined. + +1. Type `unalias s` +2. Type `s 1 by 11pm` +3. Type `schedule 1 by 11pm` + +####Result +1. After the first step, `s` is no longer an alias command for `schedule`. +2. After the second step, the pop-up window will tell you that agendum no longer recognizes the command `s`. +3. After the third step, task with id 1 is rescheduled to be by today 11pm. + +## Store +To change the default data save location, you have to start command with the keyword `store`. + +>Here is the *format*: + +>* `store ` - `` must be a valid path to a file on the local computer. If the folders specified in the new file path does not exist, they will be created. Note that the save file in the old save location remains. + +Type `> store src/test/data/ManualTesting/Test/newData.xml` + +####Result +The storage file is stored in the specified path. + +## Sync +To sync your tasks in agendum into Google calendar, you have to start command with the keyword `sync`. + +>Here is the *format*: + +>* `sync on` to turn syncing on +>* `sync off` to turn syncing off + +1. Type `sync on`. +2. Type `sync off`. + +####Result +1. After the first step, agendum will direct you to the Google Calendar authorization page in your browser. Once authorized with your Google account, there will be a pop-up window telling you that sync has been turned on, and you may now close the authorization page and go back to Agendum. After the authorization, all the tasks with an event time you added will sync to your Google Calendar, where a separate calendar called `Agendum Calendar` will appear for you to view all tasks from agendum. +2. After the second step, sync to Google Calendar will be turned off. + + + + + + diff --git a/src/test/data/XmlAddressBookStorageTest/NotXmlFormatAddressBook.xml b/src/test/data/XmlToDoListStorageTest/NotXmlFormatToDoList.xml similarity index 100% rename from src/test/data/XmlAddressBookStorageTest/NotXmlFormatAddressBook.xml rename to src/test/data/XmlToDoListStorageTest/NotXmlFormatToDoList.xml diff --git a/src/test/data/XmlUtilTest/tempAddressBook.xml b/src/test/data/XmlUtilTest/tempToDoList.xml similarity index 84% rename from src/test/data/XmlUtilTest/tempAddressBook.xml rename to src/test/data/XmlUtilTest/tempToDoList.xml index 41eeb8eb391a..3bbb72cb23ce 100644 --- a/src/test/data/XmlUtilTest/tempAddressBook.xml +++ b/src/test/data/XmlUtilTest/tempToDoList.xml @@ -1,6 +1,6 @@ - - + + 1 John Doe @@ -8,8 +8,8 @@ - + Friends - + diff --git a/src/test/data/XmlUtilTest/validAddressBook.xml b/src/test/data/XmlUtilTest/validAddressBook.xml deleted file mode 100644 index eafca730fb1e..000000000000 --- a/src/test/data/XmlUtilTest/validAddressBook.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - Hans Muster - 9482424 - hans@google.com -
4th street
-
- - Ruth Mueller - 87249245 - ruth@google.com -
81th street
-
- - Heinz Kurz - 95352563 - heinz@yahoo.com -
wall street
-
- - Cornelia Meier - 87652533 - cornelia@google.com -
10th street
-
- - Werner Meyer - 9482224 - werner@gmail.com -
michegan ave
-
- - Lydia Kunz - 9482427 - lydia@gmail.com -
little tokyo
-
- - Anna Best - 9482442 - anna@google.com -
4th street
-
- - Stefan Meier - 8482424 - stefan@mail.com -
little india
-
- - Martin Mueller - 8482131 - hans@google.com -
chicago ave
-
-
diff --git a/src/test/data/XmlUtilTest/validToDoList.xml b/src/test/data/XmlUtilTest/validToDoList.xml new file mode 100644 index 000000000000..b0f97ae433a3 --- /dev/null +++ b/src/test/data/XmlUtilTest/validToDoList.xml @@ -0,0 +1,39 @@ + + + + Hans Muster + 2016-10-10 10:10:00 + + + Ruth Mueller + 2016-10-10 10:10:00 + + + Heinz Kurz + 2016-10-10 10:10:00 + + + Cornelia Meier + 2016-10-10 10:10:00 + + + Werner Meyer + 2016-10-10 10:10:00 + + + Lydia Kunz + 2016-10-10 10:10:00 + + + Anna Best + 2016-10-10 10:10:00 + + + Stefan Meier + 2016-10-10 10:10:00 + + + Martin Mueller + 2016-10-10 10:10:00 + + diff --git a/src/test/java/guitests/AddCommandTest.java b/src/test/java/guitests/AddCommandTest.java index 3b2e1844bd0d..a9a35f23c134 100644 --- a/src/test/java/guitests/AddCommandTest.java +++ b/src/test/java/guitests/AddCommandTest.java @@ -1,53 +1,68 @@ package guitests; -import guitests.guihandles.PersonCardHandle; import org.junit.Test; -import seedu.address.logic.commands.AddCommand; -import seedu.address.commons.core.Messages; -import seedu.address.testutil.TestPerson; -import seedu.address.testutil.TestUtil; -import static org.junit.Assert.assertTrue; +import guitests.guihandles.TaskCardHandle; +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.logic.commands.AddCommand; +import seedu.agendum.testutil.TestTask; +import seedu.agendum.testutil.TestUtil; +import seedu.agendum.testutil.TypicalTestTasks; -public class AddCommandTest extends AddressBookGuiTest { +//@@author A0133367E +public class AddCommandTest extends ToDoListGuiTest { @Test - public void add() { - //add one person - TestPerson[] currentList = td.getTypicalPersons(); - TestPerson personToAdd = td.hoon; - assertAddSuccess(personToAdd, currentList); - currentList = TestUtil.addPersonsToList(currentList, personToAdd); - - //add another person - personToAdd = td.ida; - assertAddSuccess(personToAdd, currentList); - currentList = TestUtil.addPersonsToList(currentList, personToAdd); - - //add duplicate person - commandBox.runCommand(td.hoon.getAddCommand()); - assertResultMessage(AddCommand.MESSAGE_DUPLICATE_PERSON); - assertTrue(personListPanel.isListMatching(currentList)); + public void add() throws IllegalValueException { + //add one task + TestTask[] currentList = td.getTypicalTasks(); + TestTask taskToAdd = TypicalTestTasks.getFloatingTestTask(); + assertAddSuccess(taskToAdd, currentList); + currentList = TestUtil.addTasksToList(currentList, taskToAdd); + + //add another deadline task + taskToAdd = TypicalTestTasks.getDeadlineTestTask(); + assertAddSuccess(taskToAdd, currentList); + currentList = TestUtil.addTasksToList(currentList, taskToAdd); + + //add another event task + taskToAdd = TypicalTestTasks.getEventTestTask(); + assertAddSuccess(taskToAdd, currentList); + currentList = TestUtil.addTasksToList(currentList, taskToAdd); + + //add duplicate task + commandBox.runCommand(TypicalTestTasks.getFloatingTestTask().getAddCommand()); + assertResultMessage(Messages.MESSAGE_DUPLICATE_TASK); + assertAllPanelsMatch(currentList); //add to empty list - commandBox.runCommand("clear"); - assertAddSuccess(td.alice); + commandBox.runCommand("delete 1-10"); + assertAddSuccess(TypicalTestTasks.getFloatingTestTask()); //invalid command commandBox.runCommand("adds Johnny"); - assertResultMessage(Messages.MESSAGE_UNKNOWN_COMMAND); + assertResultMessage(String.format(Messages.MESSAGE_UNKNOWN_COMMAND_WITH_SUGGESTION, "add")); } - private void assertAddSuccess(TestPerson personToAdd, TestPerson... currentList) { - commandBox.runCommand(personToAdd.getAddCommand()); + private void assertAddSuccess(TestTask taskToAdd, TestTask... currentList) { + commandBox.runCommand(taskToAdd.getAddCommand()); //confirm the new card contains the right data - PersonCardHandle addedCard = personListPanel.navigateToPerson(personToAdd.getName().fullName); - assertMatching(personToAdd, addedCard); + if (taskToAdd.isCompleted()) { + TaskCardHandle addedCard = completedTasksPanel.navigateToTask(taskToAdd.getName().fullName); + assertMatching(taskToAdd, addedCard); + } else if (!taskToAdd.isCompleted() && !taskToAdd.hasTime()) { + TaskCardHandle addedCard = floatingTasksPanel.navigateToTask(taskToAdd.getName().fullName); + assertMatching(taskToAdd, addedCard); + } else if (!taskToAdd.isCompleted() && taskToAdd.hasTime()) { + TaskCardHandle addedCard = upcomingTasksPanel.navigateToTask(taskToAdd.getName().fullName); + assertMatching(taskToAdd, addedCard); + } - //confirm the list now contains all previous persons plus the new person - TestPerson[] expectedList = TestUtil.addPersonsToList(currentList, personToAdd); - assertTrue(personListPanel.isListMatching(expectedList)); + //confirm the list now contains all previous tasks plus the new task + taskToAdd.setLastUpdatedTimeToNow(); + TestTask[] expectedList = TestUtil.addTasksToList(currentList, taskToAdd); + assertAllPanelsMatch(expectedList); } - } diff --git a/src/test/java/guitests/AddressBookGuiTest.java b/src/test/java/guitests/AddressBookGuiTest.java deleted file mode 100644 index 35734932f11c..000000000000 --- a/src/test/java/guitests/AddressBookGuiTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package guitests; - -import guitests.guihandles.*; -import javafx.stage.Stage; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.rules.TestName; -import org.testfx.api.FxToolkit; -import seedu.address.TestApp; -import seedu.address.commons.core.EventsCenter; -import seedu.address.model.AddressBook; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.testutil.TestUtil; -import seedu.address.testutil.TypicalTestPersons; - -import java.util.concurrent.TimeoutException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -/** - * A GUI Test class for AddressBook. - */ -public abstract class AddressBookGuiTest { - - /* The TestName Rule makes the current test name available inside test methods */ - @Rule - public TestName name = new TestName(); - - TestApp testApp; - - protected TypicalTestPersons td = new TypicalTestPersons(); - - /* - * Handles to GUI elements present at the start up are created in advance - * for easy access from child classes. - */ - protected MainGuiHandle mainGui; - protected MainMenuHandle mainMenu; - protected PersonListPanelHandle personListPanel; - protected ResultDisplayHandle resultDisplay; - protected CommandBoxHandle commandBox; - private Stage stage; - - @BeforeClass - public static void setupSpec() { - try { - FxToolkit.registerPrimaryStage(); - FxToolkit.hideStage(); - } catch (TimeoutException e) { - e.printStackTrace(); - } - } - - @Before - public void setup() throws Exception { - FxToolkit.setupStage((stage) -> { - mainGui = new MainGuiHandle(new GuiRobot(), stage); - mainMenu = mainGui.getMainMenu(); - personListPanel = mainGui.getPersonListPanel(); - resultDisplay = mainGui.getResultDisplay(); - commandBox = mainGui.getCommandBox(); - this.stage = stage; - }); - EventsCenter.clearSubscribers(); - testApp = (TestApp) FxToolkit.setupApplication(() -> new TestApp(this::getInitialData, getDataFileLocation())); - FxToolkit.showStage(); - while (!stage.isShowing()); - mainGui.focusOnMainApp(); - } - - /** - * Override this in child classes to set the initial local data. - * Return null to use the data in the file specified in {@link #getDataFileLocation()} - */ - protected AddressBook getInitialData() { - AddressBook ab = TestUtil.generateEmptyAddressBook(); - TypicalTestPersons.loadAddressBookWithSampleData(ab); - return ab; - } - - /** - * Override this in child classes to set the data file location. - */ - protected String getDataFileLocation() { - return TestApp.SAVE_LOCATION_FOR_TESTING; - } - - @After - public void cleanup() throws TimeoutException { - FxToolkit.cleanupStages(); - } - - /** - * Asserts the person shown in the card is same as the given person - */ - public void assertMatching(ReadOnlyPerson person, PersonCardHandle card) { - assertTrue(TestUtil.compareCardAndPerson(card, person)); - } - - /** - * Asserts the size of the person list is equal to the given number. - */ - protected void assertListSize(int size) { - int numberOfPeople = personListPanel.getNumberOfPeople(); - assertEquals(size, numberOfPeople); - } - - /** - * Asserts the message shown in the Result Display area is same as the given string. - */ - protected void assertResultMessage(String expected) { - assertEquals(expected, resultDisplay.getText()); - } -} diff --git a/src/test/java/guitests/ClearCommandTest.java b/src/test/java/guitests/ClearCommandTest.java deleted file mode 100644 index 9d52b427659c..000000000000 --- a/src/test/java/guitests/ClearCommandTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package guitests; - -import org.junit.Test; - -import static org.junit.Assert.assertTrue; - -public class ClearCommandTest extends AddressBookGuiTest { - - @Test - public void clear() { - - //verify a non-empty list can be cleared - assertTrue(personListPanel.isListMatching(td.getTypicalPersons())); - assertClearCommandSuccess(); - - //verify other commands can work after a clear command - commandBox.runCommand(td.hoon.getAddCommand()); - assertTrue(personListPanel.isListMatching(td.hoon)); - commandBox.runCommand("delete 1"); - assertListSize(0); - - //verify clear command works when the list is empty - assertClearCommandSuccess(); - } - - private void assertClearCommandSuccess() { - commandBox.runCommand("clear"); - assertListSize(0); - assertResultMessage("Address book has been cleared!"); - } -} diff --git a/src/test/java/guitests/CommandBoxTest.java b/src/test/java/guitests/CommandBoxTest.java index 1379198bf8b0..a6a2c55de81e 100644 --- a/src/test/java/guitests/CommandBoxTest.java +++ b/src/test/java/guitests/CommandBoxTest.java @@ -2,21 +2,59 @@ import org.junit.Test; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.testutil.TypicalTestTasks; + import static org.junit.Assert.assertEquals; -public class CommandBoxTest extends AddressBookGuiTest { +//@@author A0148031R +public class CommandBoxTest extends ToDoListGuiTest { @Test - public void commandBox_commandSucceeds_textCleared() { - commandBox.runCommand(td.benson.getAddCommand()); + public void commandBox_CommandSucceeds_TextCleared() throws IllegalValueException { + commandBox.runCommand(TypicalTestTasks.BENSON.getAddCommand()); assertEquals(commandBox.getCommandInput(), ""); } @Test - public void commandBox_commandFails_textStays(){ + public void commandBox_CommandFails_TextStays(){ commandBox.runCommand("invalid command"); assertEquals(commandBox.getCommandInput(), "invalid command"); //TODO: confirm the text box color turns to red } + + @Test + public void commandBox_CommandHistory_Empty() { + // No previous command + commandBox.scrollToPreviousCommand(); + assertEquals(commandBox.getCommandInput(), ""); + + // No next command + commandBox.scrollToNextCommand(); + assertEquals(commandBox.getCommandInput(), ""); + } + + @Test + public void commandBox_CommandHistory_Exists() { + String addCommand = "add commandhistorytestevent"; + commandBox.runCommand(addCommand); + commandBox.runCommand("undo"); + + // Get previous undo command + commandBox.scrollToPreviousCommand(); + assertEquals(commandBox.getCommandInput(), "undo"); + + // Get previous add command + commandBox.scrollToPreviousCommand(); + assertEquals(commandBox.getCommandInput(), addCommand); + + // Get next undo command + commandBox.scrollToNextCommand(); + assertEquals(commandBox.getCommandInput(), "undo"); + + // No next command + commandBox.scrollToNextCommand(); + assertEquals(commandBox.getCommandInput(), ""); + } } diff --git a/src/test/java/guitests/DeleteCommandTest.java b/src/test/java/guitests/DeleteCommandTest.java index 10c7b9e0dbea..b252de6b898b 100644 --- a/src/test/java/guitests/DeleteCommandTest.java +++ b/src/test/java/guitests/DeleteCommandTest.java @@ -1,54 +1,65 @@ package guitests; +import java.util.ArrayList; + import org.junit.Test; -import seedu.address.testutil.TestPerson; -import seedu.address.testutil.TestUtil; -import static org.junit.Assert.assertTrue; -import static seedu.address.logic.commands.DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS; +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.logic.commands.CommandResult; +import seedu.agendum.logic.commands.DeleteCommand; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.testutil.TestTask; +import seedu.agendum.testutil.TestUtil; -public class DeleteCommandTest extends AddressBookGuiTest { +public class DeleteCommandTest extends ToDoListGuiTest { @Test - public void delete() { + public void delete() throws IllegalValueException { //delete the first in the list - TestPerson[] currentList = td.getTypicalPersons(); + TestTask[] currentList = td.getTypicalTasks(); int targetIndex = 1; assertDeleteSuccess(targetIndex, currentList); //delete the last in the list - currentList = TestUtil.removePersonFromList(currentList, targetIndex); + currentList = TestUtil.removeTaskFromList(currentList, targetIndex); targetIndex = currentList.length; assertDeleteSuccess(targetIndex, currentList); //delete from the middle of the list - currentList = TestUtil.removePersonFromList(currentList, targetIndex); + currentList = TestUtil.removeTaskFromList(currentList, targetIndex); targetIndex = currentList.length/2; assertDeleteSuccess(targetIndex, currentList); //invalid index commandBox.runCommand("delete " + currentList.length + 1); - assertResultMessage("The person index provided is invalid"); + assertResultMessage(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); } /** - * Runs the delete command to delete the person at specified index and confirms the result is correct. - * @param targetIndexOneIndexed e.g. to delete the first person in the list, 1 should be given as the target index. - * @param currentList A copy of the current list of persons (before deletion). + * Runs the delete command to delete the task at specified index and confirms the result is correct. + * @param targetIndexOneIndexed e.g. to delete the first task in the list, 1 should be given as the target index. + * @param currentList A copy of the current list of tasks (before deletion). */ - private void assertDeleteSuccess(int targetIndexOneIndexed, final TestPerson[] currentList) { - TestPerson personToDelete = currentList[targetIndexOneIndexed-1]; //-1 because array uses zero indexing - TestPerson[] expectedRemainder = TestUtil.removePersonFromList(currentList, targetIndexOneIndexed); + private void assertDeleteSuccess(int targetIndexOneIndexed, final TestTask[] currentList) { + TestTask taskToDelete = currentList[targetIndexOneIndexed-1]; //-1 because array uses zero indexing + TestTask[] expectedRemainder = TestUtil.removeTaskFromList(currentList, targetIndexOneIndexed); commandBox.runCommand("delete " + targetIndexOneIndexed); - //confirm the list now contains all previous persons except the deleted person - assertTrue(personListPanel.isListMatching(expectedRemainder)); + //confirm the list now contains all previous tasks except the deleted task + assertAllPanelsMatch(expectedRemainder); //confirm the result message is correct - assertResultMessage(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); + ArrayList deletedTaskVisibleIndices = new ArrayList<>(); + deletedTaskVisibleIndices.add(targetIndexOneIndexed); + ArrayList deletedTasks = new ArrayList<>(); + deletedTasks.add(taskToDelete); + + assertResultMessage(String.format(DeleteCommand.MESSAGE_DELETE_TASK_SUCCESS, + CommandResult.tasksToString(deletedTasks, deletedTaskVisibleIndices))); } } diff --git a/src/test/java/guitests/FindCommandTest.java b/src/test/java/guitests/FindCommandTest.java index 441a6dbed666..ecb62e291dea 100644 --- a/src/test/java/guitests/FindCommandTest.java +++ b/src/test/java/guitests/FindCommandTest.java @@ -1,26 +1,27 @@ package guitests; import org.junit.Test; -import seedu.address.commons.core.Messages; -import seedu.address.testutil.TestPerson; -import static org.junit.Assert.assertTrue; +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.testutil.TestTask; +import seedu.agendum.testutil.TypicalTestTasks; -public class FindCommandTest extends AddressBookGuiTest { +public class FindCommandTest extends ToDoListGuiTest { @Test - public void find_nonEmptyList() { + public void find_nonEmptyList() throws IllegalValueException { assertFindResult("find Mark"); //no results - assertFindResult("find Meier", td.benson, td.daniel); //multiple results + assertFindResult("find Meier", TypicalTestTasks.BENSON, TypicalTestTasks.DANIEL); //multiple results //find after deleting one result commandBox.runCommand("delete 1"); - assertFindResult("find Meier",td.daniel); + assertFindResult("find Meier", TypicalTestTasks.DANIEL); } @Test public void find_emptyList(){ - commandBox.runCommand("clear"); + commandBox.runCommand("delete 1-7"); assertFindResult("find Jean"); //no results } @@ -29,11 +30,34 @@ public void find_invalidCommand_fail() { commandBox.runCommand("findgeorge"); assertResultMessage(Messages.MESSAGE_UNKNOWN_COMMAND); } + + //@@author A0148031R + @Test + public void find_showMesssage() { + commandBox.runCommand("find Meier"); + assertShowingMessage(Messages.MESSAGE_ESCAPE_HELP_WINDOW); + assertFindResult("find Meier", TypicalTestTasks.BENSON, TypicalTestTasks.DANIEL); + } + + @Test + public void find_showMessage_fail() { + commandBox.runCommand("find2"); + assertShowingMessage(null); + } + + @Test + public void find_backToAllTasks_WithEscape() { + assertFindResult("find Meier", TypicalTestTasks.BENSON, TypicalTestTasks.DANIEL); + assertShowingMessage(Messages.MESSAGE_ESCAPE_HELP_WINDOW); + mainGui.pressEscape(); + assertAllPanelsMatch(td.getTypicalTasks()); + } - private void assertFindResult(String command, TestPerson... expectedHits ) { + //@@author + private void assertFindResult(String command, TestTask... expectedHits ) { commandBox.runCommand(command); assertListSize(expectedHits.length); - assertResultMessage(expectedHits.length + " persons listed!"); - assertTrue(personListPanel.isListMatching(expectedHits)); + assertResultMessage(String.format(Messages.MESSAGE_TASKS_LISTED_OVERVIEW, expectedHits.length)); + assertAllPanelsMatch(expectedHits); } } diff --git a/src/test/java/guitests/GuiRobot.java b/src/test/java/guitests/GuiRobot.java index 44aa9edb48aa..cca0c11d798a 100644 --- a/src/test/java/guitests/GuiRobot.java +++ b/src/test/java/guitests/GuiRobot.java @@ -2,7 +2,7 @@ import javafx.scene.input.KeyCodeCombination; import org.testfx.api.FxRobot; -import seedu.address.testutil.TestUtil; +import seedu.agendum.testutil.TestUtil; /** * Robot used to simulate user actions on the GUI. diff --git a/src/test/java/guitests/HelpWindowTest.java b/src/test/java/guitests/HelpWindowTest.java index 258d9d628d80..7dc249ad9a39 100644 --- a/src/test/java/guitests/HelpWindowTest.java +++ b/src/test/java/guitests/HelpWindowTest.java @@ -1,25 +1,41 @@ package guitests; import guitests.guihandles.HelpWindowHandle; + import org.junit.Test; import static org.junit.Assert.assertTrue; -public class HelpWindowTest extends AddressBookGuiTest { +//@@author A0148031R +public class HelpWindowTest extends ToDoListGuiTest { @Test public void openHelpWindow() { - - personListPanel.clickOnListView(); - + + assertHelpWindowOpen(mainMenu.openHelpWindowFromMenu()); + assertHelpWindowOpen(mainMenu.openHelpWindowUsingAccelerator()); - assertHelpWindowOpen(mainMenu.openHelpWindowUsingMenu()); - assertHelpWindowOpen(commandBox.runHelpCommand()); } + @Test + public void closeHelpWindow() { + commandBox.runHelpCommand(); + assertHelpWindowClose(mainMenu.closeHelpWindowUsingAccelerator()); + } + + // Tests Ctrl-H to switch between mainwindow and helpwindow + @Test + public void toggleHelpWindow() { + assertHelpWindowClose(mainMenu.toggleHelpWindow()); + } + + private void assertHelpWindowClose(HelpWindowHandle helpWindowHandle) { + assertTrue(helpWindowHandle.isWindowClose()); + } + private void assertHelpWindowOpen(HelpWindowHandle helpWindowHandle) { assertTrue(helpWindowHandle.isWindowOpen()); helpWindowHandle.closeWindow(); diff --git a/src/test/java/guitests/LoadCommandTest.java b/src/test/java/guitests/LoadCommandTest.java new file mode 100644 index 000000000000..14439516e872 --- /dev/null +++ b/src/test/java/guitests/LoadCommandTest.java @@ -0,0 +1,82 @@ +package guitests; + +import java.io.File; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.logic.commands.LoadCommand; +import seedu.agendum.model.ToDoList; +import seedu.agendum.model.task.Name; +import seedu.agendum.model.task.Task; +import seedu.agendum.storage.XmlToDoListStorage; + +//@@author A0148095X +public class LoadCommandTest extends ToDoListGuiTest { + + private final String command = LoadCommand.COMMAND_WORD + " "; + private final String fileThatExists = "data/test/FileThatExists.xml"; + private final String fileThatDoesNotExist = "data/test/DoesNotExist.xml"; + private final String fileInWrongFormat = "data/test/WrongFormat.xml"; + private final String missingFileType = "data/test/invalid"; + private final String missingFileName = "data/test/.bad"; + + @Before + public void setUp() throws Exception { + super.setUp(); + + // setup storage file + Task toBeAdded = new Task(new Name("test")); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + XmlToDoListStorage xmltdls = new XmlToDoListStorage(fileThatExists); + xmltdls.saveToDoList(expectedTDL); + + // create empty file + FileUtil.createFile(new File(fileInWrongFormat)); + } + + @After + public void clean() throws Exception { + // cleanup + FileUtil.deleteFile(fileThatExists); + FileUtil.deleteFile(fileInWrongFormat); + } + + @Test + public void load_pathValidFileExists_messageSuccess() { + // load from an existing file + commandBox.runCommand(command + fileThatExists); + assertResultMessage(String.format(LoadCommand.MESSAGE_SUCCESS, fileThatExists)); + } + + @Test + public void load_pathValidFileDoesNotExist_messageFileDoesNotExist() { + // load from a non-existing file + commandBox.runCommand(command + fileThatDoesNotExist); + assertResultMessage(String.format(LoadCommand.MESSAGE_FILE_DOES_NOT_EXIST, fileThatDoesNotExist)); + } + + @Test + public void load_pathValidFileWrongFormat_messageFileWrongFormat() { + // file in wrong format + commandBox.runCommand(command + fileInWrongFormat); + assertResultMessage(String.format(LoadCommand.MESSAGE_FILE_WRONG_FORMAT, fileInWrongFormat)); + } + + @Test + public void load_fileTypeInvalid_messagePathInvalid() { + // invalid file type + commandBox.runCommand(command + missingFileType); + assertResultMessage(String.format(LoadCommand.MESSAGE_PATH_INVALID, missingFileType)); + } + + @Test + public void load_fileNameInvalid_messagePathInvalid() { + // invalid file name + commandBox.runCommand(command + missingFileName); + assertResultMessage(String.format(LoadCommand.MESSAGE_PATH_INVALID, missingFileName)); + } +} diff --git a/src/test/java/guitests/MarkCommandTest.java b/src/test/java/guitests/MarkCommandTest.java new file mode 100644 index 000000000000..2ec565072ee4 --- /dev/null +++ b/src/test/java/guitests/MarkCommandTest.java @@ -0,0 +1,62 @@ +package guitests; + +import org.junit.Test; + +import guitests.guihandles.TaskCardHandle; +import seedu.agendum.commons.core.Messages; +import seedu.agendum.logic.commands.MarkCommand; +import seedu.agendum.testutil.TestTask; + +//@@author A0148031R +public class MarkCommandTest extends ToDoListGuiTest{ + + @Test + public void mark_nonEmptytask_succeed() { + TestTask[] currentList = td.getTypicalTasks(); + currentList[0].markAsCompleted(); + TestTask taskToMark = currentList[0]; + assertMarkSuccess("mark 1", taskToMark, currentList); + } + + @Test + public void mark_nonEmptytask_duplicates() { + assertMarkDuplicates("mark 7"); + } + + @Test + public void mark_emptytask() { + assetMarkEmptyTask("mark 8"); + } + + private void assertMarkSuccess(String command, TestTask taskToMark, TestTask... currentList) { + commandBox.runCommand(command); + + //confirm the new card contains the right data + if (taskToMark.isCompleted()) { + TaskCardHandle addedCard = completedTasksPanel.navigateToTask(taskToMark.getName().fullName); + assertMatching(taskToMark, addedCard); + } else if (!taskToMark.isCompleted() && !taskToMark.hasTime()) { + TaskCardHandle addedCard = floatingTasksPanel.navigateToTask(taskToMark.getName().fullName); + assertMatching(taskToMark, addedCard); + } else if (!taskToMark.isCompleted() && taskToMark.hasTime()) { + TaskCardHandle addedCard = upcomingTasksPanel.navigateToTask(taskToMark.getName().fullName); + assertMatching(taskToMark, addedCard); + } + + //confirm the list now contains all previous tasks plus the new task + taskToMark.setLastUpdatedTimeToNow(); + TestTask[] expectedList = currentList; + assertAllPanelsMatch(expectedList); + assertResultMessage(MarkCommand.MESSAGE_MARK_TASK_SUCCESS); + } + + private void assertMarkDuplicates(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_DUPLICATE_TASK); + } + + private void assetMarkEmptyTask(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } +} diff --git a/src/test/java/guitests/RenameCommandTest.java b/src/test/java/guitests/RenameCommandTest.java new file mode 100644 index 000000000000..24d7cce60379 --- /dev/null +++ b/src/test/java/guitests/RenameCommandTest.java @@ -0,0 +1,69 @@ +package guitests; + +import org.junit.Test; + +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.logic.commands.RenameCommand; +import seedu.agendum.model.task.Name; +import seedu.agendum.testutil.TestTask; +import seedu.agendum.testutil.TestUtil; + +public class RenameCommandTest extends ToDoListGuiTest { + + @Test + public void rename() throws IllegalValueException { + + //rename the first in the list + TestTask[] currentList = td.getTypicalTasks(); + int targetIndex = 1; + assertRenameSuccess(targetIndex, currentList); + + //rename the last in the list + targetIndex = currentList.length; + assertRenameSuccess(targetIndex, currentList); + + //rename task in the middle of the list + targetIndex = currentList.length/2; + assertRenameSuccess(targetIndex, currentList); + + //invalid index + commandBox.runCommand("rename " + currentList.length + 1 + " new task name"); + assertResultMessage(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + + //duplicate task + commandBox.runCommand("rename " + currentList.length + " " + currentList[targetIndex].getName().toString()); + assertResultMessage(Messages.MESSAGE_DUPLICATE_TASK); + assertAllPanelsMatch(currentList); + } + + /** + * Runs the rename command to rename the task at specified index and confirms the result is correct. + * @param targetIndexOneIndexed e.g. to rename the first task in the list, 1 should be given as the target index. + * @param currentList - list of tasks (before renaming). + */ + private void assertRenameSuccess(int targetIndexOneIndexed, TestTask[] currentList) { + TestTask taskToRename = currentList[targetIndexOneIndexed - 1]; //-1 because array uses zero indexing + String newTaskName = taskToRename.getName().toString() + " renamed"; + + try { + TestTask renamedTask = new TestTask(taskToRename); + renamedTask.setName(new Name(newTaskName)); + TestTask[] expectedList = TestUtil.replaceTaskFromList(currentList, renamedTask, targetIndexOneIndexed - 1); + + commandBox.runCommand("rename " + targetIndexOneIndexed + " " + newTaskName); + + //confirm the list now contains all previous tasks with the specified task's name updated + assertAllPanelsMatch(expectedList); + + //confirm the result message is correct + assertResultMessage(String.format(RenameCommand.MESSAGE_SUCCESS, newTaskName)); + + } catch (IllegalValueException e) { + e.printStackTrace(); + assert false : "not possible"; + } + } + +} + diff --git a/src/test/java/guitests/SelectCommandTest.java b/src/test/java/guitests/SelectCommandTest.java deleted file mode 100644 index 5273552056ce..000000000000 --- a/src/test/java/guitests/SelectCommandTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package guitests; - -import org.junit.Test; -import seedu.address.model.person.ReadOnlyPerson; - -import static org.junit.Assert.assertEquals; - -public class SelectCommandTest extends AddressBookGuiTest { - - - @Test - public void selectPerson_nonEmptyList() { - - assertSelectionInvalid(10); //invalid index - assertNoPersonSelected(); - - assertSelectionSuccess(1); //first person in the list - int personCount = td.getTypicalPersons().length; - assertSelectionSuccess(personCount); //last person in the list - int middleIndex = personCount / 2; - assertSelectionSuccess(middleIndex); //a person in the middle of the list - - assertSelectionInvalid(personCount + 1); //invalid index - assertPersonSelected(middleIndex); //assert previous selection remains - - /* Testing other invalid indexes such as -1 should be done when testing the SelectCommand */ - } - - @Test - public void selectPerson_emptyList(){ - commandBox.runCommand("clear"); - assertListSize(0); - assertSelectionInvalid(1); //invalid index - } - - private void assertSelectionInvalid(int index) { - commandBox.runCommand("select " + index); - assertResultMessage("The person index provided is invalid"); - } - - private void assertSelectionSuccess(int index) { - commandBox.runCommand("select " + index); - assertResultMessage("Selected Person: "+index); - assertPersonSelected(index); - } - - private void assertPersonSelected(int index) { - assertEquals(personListPanel.getSelectedPersons().size(), 1); - ReadOnlyPerson selectedPerson = personListPanel.getSelectedPersons().get(0); - assertEquals(personListPanel.getPerson(index-1), selectedPerson); - //TODO: confirm the correct page is loaded in the Browser Panel - } - - private void assertNoPersonSelected() { - assertEquals(personListPanel.getSelectedPersons().size(), 0); - } - -} diff --git a/src/test/java/guitests/StoreCommandTest.java b/src/test/java/guitests/StoreCommandTest.java new file mode 100644 index 000000000000..bd36cbba32b2 --- /dev/null +++ b/src/test/java/guitests/StoreCommandTest.java @@ -0,0 +1,57 @@ +package guitests; + +import java.io.File; +import java.io.IOException; + +import org.junit.Test; + +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.exceptions.FileDeletionException; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.logic.commands.StoreCommand; + +//@@author A0148095X +public class StoreCommandTest extends ToDoListGuiTest { + + private final String validLocation = "data/test.xml"; + private final String badLocation = "test/.xml"; + private final String inaccessibleLocation = "C:/windows/system32/agendum/todolist.xml"; + + @Test + public void store_validLocation_messageSuccess() { + //save to a valid directory + commandBox.runCommand("store " + validLocation); + assertResultMessage(String.format(StoreCommand.MESSAGE_SUCCESS, validLocation)); + } + + @Test + public void store_defaultLocation_messageSuccessDefaultLocation() { + //save to default directory + commandBox.runCommand("store default"); + assertResultMessage(String.format(StoreCommand.MESSAGE_LOCATION_DEFAULT, Config.DEFAULT_SAVE_LOCATION)); + } + + @Test + public void store_invalidLocation_messageInvalidPath() { + //invalid Location + commandBox.runCommand("store " + badLocation); + assertResultMessage(StoreCommand.MESSAGE_PATH_WRONG_FORMAT); + } + + @Test + public void store_inaccessibleLocation_messageLocationInaccessible() { + //inaccessible location + commandBox.runCommand("store " + inaccessibleLocation); + //assertResultMessage(StoreCommand.MESSAGE_LOCATION_INACCESSIBLE); + } + + @Test + public void store_fileExists_messageFileExists() throws IOException, FileDeletionException { + //file exists + FileUtil.createIfMissing(new File(validLocation)); + commandBox.runCommand("store " + validLocation); + assertResultMessage(StoreCommand.MESSAGE_FILE_EXISTS); + FileUtil.deleteFile(validLocation); + + } +} \ No newline at end of file diff --git a/src/test/java/guitests/ToDoListGuiTest.java b/src/test/java/guitests/ToDoListGuiTest.java new file mode 100644 index 000000000000..311db7b4f9d0 --- /dev/null +++ b/src/test/java/guitests/ToDoListGuiTest.java @@ -0,0 +1,148 @@ +package guitests; + +import guitests.guihandles.*; +import javafx.stage.Stage; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestName; +import org.testfx.api.FxToolkit; +import seedu.agendum.TestApp; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.model.ToDoList; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.testutil.TestTask; +import seedu.agendum.testutil.TestUtil; +import seedu.agendum.testutil.TypicalTestTasks; + +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * A GUI Test class for ToDoList. + */ +public abstract class ToDoListGuiTest { + + /* The TestName Rule makes the current test name available inside test methods */ + @Rule + public TestName name = new TestName(); + + TestApp testApp; + + protected TypicalTestTasks td = new TypicalTestTasks(); + + /* + * Handles to GUI elements present at the start up are created in advance + * for easy access from child classes. + */ + protected MainGuiHandle mainGui; + protected MainMenuHandle mainMenu; + protected UpcomingTasksHandle upcomingTasksPanel; + protected FloatingTasksPanelHandle floatingTasksPanel; + protected CompletedTasksPanelHandle completedTasksPanel; + protected ResultDisplayHandle resultDisplay; + protected MessageDisplayHandle messageDisplay; + protected CommandBoxHandle commandBox; + private Stage stage; + + @BeforeClass + public static void setupSpec() { + try { + FxToolkit.registerPrimaryStage(); + FxToolkit.hideStage(); + } catch (TimeoutException e) { + e.printStackTrace(); + } + } + + @Before + public void setUp() throws Exception { + FxToolkit.setupStage((stage) -> { + mainGui = new MainGuiHandle(new GuiRobot(), stage); + mainMenu = mainGui.getMainMenu(); + upcomingTasksPanel = mainGui.getDoItSoonPanel(); + floatingTasksPanel = mainGui.getDoItAnytimePanel(); + completedTasksPanel = mainGui.getCompletedTasksPanel(); + resultDisplay = mainGui.getResultDisplay(); + messageDisplay = mainGui.getMessageDisplay(); + commandBox = mainGui.getCommandBox(); + this.stage = stage; + }); + EventsCenter.clearSubscribers(); + testApp = (TestApp) FxToolkit.setupApplication(() -> new TestApp(this::getInitialData, getDataFileLocation())); + FxToolkit.showStage(); + while (!stage.isShowing()); + mainGui.focusOnMainApp(); + } + + /** + * Override this in child classes to set the initial local data. + * Return null to use the data in the file specified in {@link #getDataFileLocation()} + */ + protected ToDoList getInitialData() { + ToDoList ab = TestUtil.generateEmptyToDoList(); + TypicalTestTasks.loadToDoListWithSampleData(ab); + return ab; + } + + /** + * Override this in child classes to set the data file location. + */ + protected String getDataFileLocation() { + return TestApp.SAVE_LOCATION_FOR_TESTING; + } + + @After + public void cleanup() throws TimeoutException { + FxToolkit.cleanupStages(); + } + + /** + * Asserts the task shown in the card is same as the given task + */ + public void assertMatching(ReadOnlyTask task, TaskCardHandle card) { + assertTrue(TestUtil.compareCardAndTask(card, task)); + } + + /** + * Asserts the size of the task list is equal to the given number. + */ + protected void assertListSize(int size) { + int numberOfTasks = upcomingTasksPanel.getNumberOfTasks() + + floatingTasksPanel.getNumberOfTasks() + + completedTasksPanel.getNumberOfTasks(); + assertEquals(size, numberOfTasks); + } + + //@@author A0148031R + /** + * Asserts the message shown in the Result Display area is same as the given string. + */ + protected void assertResultMessage(String expected) { + assertEquals(expected, resultDisplay.getText()); + } + + /** + * Asserts the message shown in the Message Display area is same as the given string. + */ + protected void assertShowingMessage(String expected) { + assertEquals(expected, messageDisplay.getText()); + } + + //@@author A0133367E + /** + * Asserts the tasks shown in each panel will match + */ + protected void assertAllPanelsMatch(TestTask[] expectedList) { + TestUtil.sortTasks(expectedList); + TestTask[] expectedDoItSoonTasks = TestUtil.getUpcomingTasks(expectedList); + TestTask[] expectedDoItAnytimeTasks = TestUtil.getFloatingTasks(expectedList); + TestTask[] expectedDoneTasks = TestUtil.getCompletedTasks(expectedList); + assertTrue(upcomingTasksPanel.isListMatching(expectedDoItSoonTasks)); + assertTrue(floatingTasksPanel.isListMatching(expectedDoItAnytimeTasks)); + assertTrue(completedTasksPanel.isListMatching(expectedDoneTasks)); + } +} diff --git a/src/test/java/guitests/UnmarkCommandTest.java b/src/test/java/guitests/UnmarkCommandTest.java new file mode 100644 index 000000000000..8282efd019d2 --- /dev/null +++ b/src/test/java/guitests/UnmarkCommandTest.java @@ -0,0 +1,61 @@ +package guitests; + +import org.junit.Test; + +import guitests.guihandles.TaskCardHandle; +import seedu.agendum.commons.core.Messages; +import seedu.agendum.logic.commands.UnmarkCommand; +import seedu.agendum.testutil.TestTask; + +//@@author A0148031R +public class UnmarkCommandTest extends ToDoListGuiTest { + @Test + public void unmark_nonEmptytask_succeed() { + TestTask[] currentList = td.getTypicalTasks(); + TestTask taskToUnmark = currentList[0]; + commandBox.runCommand("mark 1"); + assertUnmarkSuccess("unmark 7", taskToUnmark, currentList); + } + + @Test + public void unmark_nonEmptytask_duplicates() { + assertUnmarkDuplicates("unmark 1"); + } + + @Test + public void unmark_emptytask() { + assetUnmarkEmptyTask("unmark 8"); + } + + private void assertUnmarkSuccess(String command, TestTask taskToUnmark, TestTask... currentList) { + commandBox.runCommand(command); + + //confirm the new card contains the right data + if (taskToUnmark.isCompleted()) { + TaskCardHandle addedCard = completedTasksPanel.navigateToTask(taskToUnmark.getName().fullName); + assertMatching(taskToUnmark, addedCard); + } else if (!taskToUnmark.isCompleted() && !taskToUnmark.hasTime()) { + TaskCardHandle addedCard = floatingTasksPanel.navigateToTask(taskToUnmark.getName().fullName); + assertMatching(taskToUnmark, addedCard); + } else if (!taskToUnmark.isCompleted() && taskToUnmark.hasTime()) { + TaskCardHandle addedCard = upcomingTasksPanel.navigateToTask(taskToUnmark.getName().fullName); + assertMatching(taskToUnmark, addedCard); + } + + //confirm the list now contains all previous tasks plus the new task + taskToUnmark.setLastUpdatedTimeToNow(); + TestTask[] expectedList = currentList; + assertAllPanelsMatch(expectedList); + assertResultMessage(UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS); + } + + private void assertUnmarkDuplicates(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_DUPLICATE_TASK); + } + + private void assetUnmarkEmptyTask(String command) { + commandBox.runCommand(command); + assertResultMessage(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } +} diff --git a/src/test/java/guitests/guihandles/CommandBoxHandle.java b/src/test/java/guitests/guihandles/CommandBoxHandle.java index dcd3155636cd..4fdea069ca04 100644 --- a/src/test/java/guitests/guihandles/CommandBoxHandle.java +++ b/src/test/java/guitests/guihandles/CommandBoxHandle.java @@ -1,8 +1,11 @@ package guitests.guihandles; import guitests.GuiRobot; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; import javafx.stage.Stage; +//@@author A0148031R /** * A handle to the Command Box in the GUI. */ @@ -28,12 +31,25 @@ public String getCommandInput() { public void runCommand(String command) { enterCommand(command); pressEnter(); - guiRobot.sleep(200); //Give time for the command to take effect + + //Give time for the command to take effect + guiRobot.sleep(2000); } - + public HelpWindowHandle runHelpCommand() { enterCommand("help"); pressEnter(); return new HelpWindowHandle(guiRobot, primaryStage); } + + public void scrollToPreviousCommand() { + guiRobot.push(new KeyCodeCombination(KeyCode.UP)); + guiRobot.sleep(200); + } + + public void scrollToNextCommand() { + guiRobot.push(new KeyCodeCombination(KeyCode.DOWN)); + guiRobot.sleep(200); + } + } diff --git a/src/test/java/guitests/guihandles/CompletedTasksPanelHandle.java b/src/test/java/guitests/guihandles/CompletedTasksPanelHandle.java new file mode 100644 index 000000000000..7180423a1f2c --- /dev/null +++ b/src/test/java/guitests/guihandles/CompletedTasksPanelHandle.java @@ -0,0 +1,168 @@ +package guitests.guihandles; + +import guitests.GuiRobot; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ListView; +import javafx.stage.Stage; +import seedu.agendum.TestApp; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; +import seedu.agendum.testutil.TestUtil; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.assertTrue; + +/** + * Provides a handle for the panel containing the task list. + */ +public class CompletedTasksPanelHandle extends GuiHandle { + + public static final int NOT_FOUND = -1; + public static final String CARD_PANE_ID = "#cardPane"; + + private static final String TASK_LIST_VIEW_ID = "#completedTasksListView"; + + public CompletedTasksPanelHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public List getSelectedTasks() { + ListView taskList = getListView(); + return taskList.getSelectionModel().getSelectedItems(); + } + + public ListView getListView() { + return (ListView) getNode(TASK_LIST_VIEW_ID); + } + + /** + * Returns true if the list is showing the task details correctly and in correct order. + * @param tasks A list of tasks in the correct order. + */ + public boolean isListMatching(ReadOnlyTask... tasks) { + return this.isListMatching(0, tasks); + } + + public void clickOnListView() { + Point2D point= TestUtil.getScreenMidPoint(getListView()); + guiRobot.clickOn(point.getX(), point.getY()); + } + + /** + * Returns true if the {@code tasks} appear as the sub list (in that order) at position {@code startPosition}. + */ + public boolean containsInOrder(int startPosition, ReadOnlyTask... tasks) { + List tasksInList = getListView().getItems(); + + // Return false if the list in panel is too short to contain the given list + if (startPosition + tasks.length > tasksInList.size()){ + return false; + } + + // Return false if any of the tasks doesn't match + for (int i = 0; i < tasks.length; i++) { + if (!tasksInList.get(startPosition + i).getName().fullName.equals(tasks[i].getName().fullName)){ + return false; + } + } + + return true; + } + + /** + * Returns true if the list is showing the task details correctly and in correct order. + * @param startPosition The starting position of the sub list. + * @param tasks A list of tasks in the correct order. + */ + public boolean isListMatching(int startPosition, ReadOnlyTask... tasks) throws IllegalArgumentException { + if (tasks.length + startPosition != getListView().getItems().size()) { + throw new IllegalArgumentException("List size mismatched\n" + + "Expected " + (getListView().getItems().size() - 1) + " tasks"); + } + assertTrue(this.containsInOrder(startPosition, tasks)); + for (int i = 0; i < tasks.length; i++) { + final int scrollTo = i + startPosition; + guiRobot.interact(() -> getListView().scrollTo(scrollTo)); + guiRobot.sleep(200); + if (!TestUtil.compareCardAndTask(getTaskCardHandle(startPosition + i), tasks[i])) { + return false; + } + } + return true; + } + + + public TaskCardHandle navigateToTask(String name) { + guiRobot.sleep(500); //Allow a bit of time for the list to be updated + final Optional task = getListView().getItems().stream().filter(p -> p.getName().fullName.equals(name)).findAny(); + if (!task.isPresent()) { + throw new IllegalStateException("Name not found: " + name); + } + + return navigateToTask(task.get()); + } + + /** + * Navigates the listview to display and select the task. + */ + public TaskCardHandle navigateToTask(ReadOnlyTask task) { + int index = getTaskIndex(task); + + guiRobot.interact(() -> { + getListView().scrollTo(index); + guiRobot.sleep(150); + getListView().getSelectionModel().select(index); + }); + guiRobot.sleep(100); + return getTaskCardHandle(task); + } + + + /** + * Returns the position of the task given, {@code NOT_FOUND} if not found in the list. + */ + public int getTaskIndex(ReadOnlyTask targetTask) { + List tasksInList = getListView().getItems(); + for (int i = 0; i < tasksInList.size(); i++) { + if(tasksInList.get(i).getName().equals(targetTask.getName())){ + return i; + } + } + return NOT_FOUND; + } + + /** + * Gets a task from the list by index + */ + public ReadOnlyTask getTask(int index) { + return getListView().getItems().get(index); + } + + public TaskCardHandle getTaskCardHandle(int index) { + return getTaskCardHandle(new Task(getListView().getItems().get(index))); + } + + public TaskCardHandle getTaskCardHandle(ReadOnlyTask task) { + Set nodes = getAllCardNodes(); + Optional taskCardNode = nodes.stream() + .filter(n -> new TaskCardHandle(guiRobot, primaryStage, n).isSameTask(task)) + .findFirst(); + if (taskCardNode.isPresent()) { + return new TaskCardHandle(guiRobot, primaryStage, taskCardNode.get()); + } else { + return null; + } + } + + protected Set getAllCardNodes() { + return guiRobot.lookup(CARD_PANE_ID).queryAll(); + } + + public int getNumberOfTasks() { + return getListView().getItems().size(); + } +} diff --git a/src/test/java/guitests/guihandles/FloatingTasksPanelHandle.java b/src/test/java/guitests/guihandles/FloatingTasksPanelHandle.java new file mode 100644 index 000000000000..d481b2cf2033 --- /dev/null +++ b/src/test/java/guitests/guihandles/FloatingTasksPanelHandle.java @@ -0,0 +1,168 @@ +package guitests.guihandles; + +import guitests.GuiRobot; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ListView; +import javafx.stage.Stage; +import seedu.agendum.TestApp; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; +import seedu.agendum.testutil.TestUtil; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.assertTrue; + +/** + * Provides a handle for the panel containing the task list. + */ +public class FloatingTasksPanelHandle extends GuiHandle { + + public static final int NOT_FOUND = -1; + public static final String CARD_PANE_ID = "#cardPane"; + + private static final String TASK_LIST_VIEW_ID = "#floatingTasksListView"; + + public FloatingTasksPanelHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public List getSelectedTasks() { + ListView taskList = getListView(); + return taskList.getSelectionModel().getSelectedItems(); + } + + public ListView getListView() { + return (ListView) getNode(TASK_LIST_VIEW_ID); + } + + /** + * Returns true if the list is showing the task details correctly and in correct order. + * @param tasks A list of tasks in the correct order. + */ + public boolean isListMatching(ReadOnlyTask... tasks) { + return this.isListMatching(0, tasks); + } + + public void clickOnListView() { + Point2D point= TestUtil.getScreenMidPoint(getListView()); + guiRobot.clickOn(point.getX(), point.getY()); + } + + /** + * Returns true if the {@code tasks} appear as the sub list (in that order) at position {@code startPosition}. + */ + public boolean containsInOrder(int startPosition, ReadOnlyTask... tasks) { + List tasksInList = getListView().getItems(); + + // Return false if the list in panel is too short to contain the given list + if (startPosition + tasks.length > tasksInList.size()){ + return false; + } + + // Return false if any of the tasks doesn't match + for (int i = 0; i < tasks.length; i++) { + if (!tasksInList.get(startPosition + i).getName().fullName.equals(tasks[i].getName().fullName)){ + return false; + } + } + + return true; + } + + /** + * Returns true if the list is showing the task details correctly and in correct order. + * @param startPosition The starting position of the sub list. + * @param tasks A list of tasks in the correct order. + */ + public boolean isListMatching(int startPosition, ReadOnlyTask... tasks) throws IllegalArgumentException { + if (tasks.length + startPosition != getListView().getItems().size()) { + throw new IllegalArgumentException("List size mismatched\n" + + "Expected " + (getListView().getItems().size() - 1) + " tasks"); + } + assertTrue(this.containsInOrder(startPosition, tasks)); + for (int i = 0; i < tasks.length; i++) { + final int scrollTo = i + startPosition; + guiRobot.interact(() -> getListView().scrollTo(scrollTo)); + guiRobot.sleep(500); + if (!TestUtil.compareCardAndTask(getTaskCardHandle(startPosition + i), tasks[i])) { + return false; + } + } + return true; + } + + + public TaskCardHandle navigateToTask(String name) { + guiRobot.sleep(500); //Allow a bit of time for the list to be updated + final Optional task = getListView().getItems().stream().filter(p -> p.getName().fullName.equals(name)).findAny(); + if (!task.isPresent()) { + throw new IllegalStateException("Name not found: " + name); + } + + return navigateToTask(task.get()); + } + + /** + * Navigates the listview to display and select the task. + */ + public TaskCardHandle navigateToTask(ReadOnlyTask task) { + int index = getTaskIndex(task); + + guiRobot.interact(() -> { + getListView().scrollTo(index); + guiRobot.sleep(150); + getListView().getSelectionModel().select(index); + }); + guiRobot.sleep(100); + return getTaskCardHandle(task); + } + + + /** + * Returns the position of the task given, {@code NOT_FOUND} if not found in the list. + */ + public int getTaskIndex(ReadOnlyTask targetTask) { + List tasksInList = getListView().getItems(); + for (int i = 0; i < tasksInList.size(); i++) { + if(tasksInList.get(i).getName().equals(targetTask.getName())){ + return i; + } + } + return NOT_FOUND; + } + + /** + * Gets a task from the list by index + */ + public ReadOnlyTask getTask(int index) { + return getListView().getItems().get(index); + } + + public TaskCardHandle getTaskCardHandle(int index) { + return getTaskCardHandle(new Task(getListView().getItems().get(index))); + } + + public TaskCardHandle getTaskCardHandle(ReadOnlyTask task) { + Set nodes = getAllCardNodes(); + Optional taskCardNode = nodes.stream() + .filter(n -> new TaskCardHandle(guiRobot, primaryStage, n).isSameTask(task)) + .findFirst(); + if (taskCardNode.isPresent()) { + return new TaskCardHandle(guiRobot, primaryStage, taskCardNode.get()); + } else { + return null; + } + } + + protected Set getAllCardNodes() { + return guiRobot.lookup(CARD_PANE_ID).queryAll(); + } + + public int getNumberOfTasks() { + return getListView().getItems().size(); + } +} diff --git a/src/test/java/guitests/guihandles/GuiHandle.java b/src/test/java/guitests/guihandles/GuiHandle.java index 5e7e0f6de911..376153bf5929 100644 --- a/src/test/java/guitests/guihandles/GuiHandle.java +++ b/src/test/java/guitests/guihandles/GuiHandle.java @@ -7,8 +7,8 @@ import javafx.scene.input.KeyCode; import javafx.stage.Stage; import javafx.stage.Window; -import seedu.address.TestApp; -import seedu.address.commons.core.LogsCenter; +import seedu.agendum.TestApp; +import seedu.agendum.commons.core.LogsCenter; import java.util.logging.Logger; @@ -19,14 +19,16 @@ public class GuiHandle { protected final GuiRobot guiRobot; protected final Stage primaryStage; protected final String stageTitle; - + private static final String HELP_WINDOW_TITLE = "Help"; private final Logger logger = LogsCenter.getLogger(this.getClass()); public GuiHandle(GuiRobot guiRobot, Stage primaryStage, String stageTitle) { this.guiRobot = guiRobot; this.primaryStage = primaryStage; this.stageTitle = stageTitle; - focusOnSelf(); + if(stageTitle == null || !stageTitle.equals(HELP_WINDOW_TITLE)) { + focusOnSelf(); + } } public void focusOnWindow(String stageTitle) { @@ -62,6 +64,10 @@ protected void setTextField(String textFieldId, String newText) { public void pressEnter() { guiRobot.type(KeyCode.ENTER).sleep(500); } + + public void pressEscape() { + guiRobot.type(KeyCode.ESCAPE).sleep(500); + } protected String getTextFromLabel(String fieldId, Node parentNode) { return ((Label) guiRobot.from(parentNode).lookup(fieldId).tryQuery().get()).getText(); diff --git a/src/test/java/guitests/guihandles/HelpWindowHandle.java b/src/test/java/guitests/guihandles/HelpWindowHandle.java index 3931c5fb24b7..77a769759a7b 100644 --- a/src/test/java/guitests/guihandles/HelpWindowHandle.java +++ b/src/test/java/guitests/guihandles/HelpWindowHandle.java @@ -3,6 +3,7 @@ import guitests.GuiRobot; import javafx.stage.Stage; +//@@author A0148031R /** * Provides a handle to the help window of the app. */ @@ -17,11 +18,21 @@ public HelpWindowHandle(GuiRobot guiRobot, Stage primaryStage) { } public boolean isWindowOpen() { - return getNode(HELP_WINDOW_ROOT_FIELD_ID) != null; + return getNode(HELP_WINDOW_ROOT_FIELD_ID) != null + && getNode(HELP_WINDOW_ROOT_FIELD_ID).getParent() != null; } + public boolean isWindowClose() { + try { + getNode(HELP_WINDOW_ROOT_FIELD_ID); + } catch (IllegalStateException e) { + return true; + } + return false; + } + public void closeWindow() { - super.closeWindow(); + super.pressEscape(); guiRobot.sleep(500); } diff --git a/src/test/java/guitests/guihandles/MainGuiHandle.java b/src/test/java/guitests/guihandles/MainGuiHandle.java index 45802c5135c7..5448c618bec6 100644 --- a/src/test/java/guitests/guihandles/MainGuiHandle.java +++ b/src/test/java/guitests/guihandles/MainGuiHandle.java @@ -2,8 +2,9 @@ import guitests.GuiRobot; import javafx.stage.Stage; -import seedu.address.TestApp; +import seedu.agendum.TestApp; +//@@author A0148031R /** * Provides a handle for the main GUI. */ @@ -13,13 +14,25 @@ public MainGuiHandle(GuiRobot guiRobot, Stage primaryStage) { super(guiRobot, primaryStage, TestApp.APP_TITLE); } - public PersonListPanelHandle getPersonListPanel() { - return new PersonListPanelHandle(guiRobot, primaryStage); + public UpcomingTasksHandle getDoItSoonPanel() { + return new UpcomingTasksHandle(guiRobot, primaryStage); + } + + public FloatingTasksPanelHandle getDoItAnytimePanel() { + return new FloatingTasksPanelHandle(guiRobot, primaryStage); + } + + public CompletedTasksPanelHandle getCompletedTasksPanel() { + return new CompletedTasksPanelHandle(guiRobot, primaryStage); } public ResultDisplayHandle getResultDisplay() { return new ResultDisplayHandle(guiRobot, primaryStage); } + + public MessageDisplayHandle getMessageDisplay() { + return new MessageDisplayHandle(guiRobot, primaryStage); + } public CommandBoxHandle getCommandBox() { return new CommandBoxHandle(guiRobot, primaryStage, TestApp.APP_TITLE); @@ -28,5 +41,4 @@ public CommandBoxHandle getCommandBox() { public MainMenuHandle getMainMenu() { return new MainMenuHandle(guiRobot, primaryStage); } - } diff --git a/src/test/java/guitests/guihandles/MainMenuHandle.java b/src/test/java/guitests/guihandles/MainMenuHandle.java index 0aeb047a0e1d..bc97ec5bf2c7 100644 --- a/src/test/java/guitests/guihandles/MainMenuHandle.java +++ b/src/test/java/guitests/guihandles/MainMenuHandle.java @@ -2,15 +2,21 @@ import guitests.GuiRobot; import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.stage.Stage; -import seedu.address.TestApp; +import seedu.agendum.TestApp; import java.util.Arrays; +//@@author A0148031R /** * Provides a handle to the main menu of the app. */ public class MainMenuHandle extends GuiHandle { + private static final String HELP = "Help"; + private static final String HELP_MENU_ITEM = "Help Ctrl-H"; + public MainMenuHandle(GuiRobot guiRobot, Stage primaryStage) { super(guiRobot, primaryStage, TestApp.APP_TITLE); } @@ -20,18 +26,46 @@ public GuiHandle clickOn(String... menuText) { return this; } - public HelpWindowHandle openHelpWindowUsingMenu() { - clickOn("Help", "F1"); + public HelpWindowHandle openHelpWindowFromMenu() { + useMenuItemToOpenHelpWindow(); return new HelpWindowHandle(guiRobot, primaryStage); } - + public HelpWindowHandle openHelpWindowUsingAccelerator() { - useF1Accelerator(); + useAcceleratorToOpenHelpWindow(); return new HelpWindowHandle(guiRobot, primaryStage); } - - private void useF1Accelerator() { - guiRobot.push(KeyCode.F1); + + public HelpWindowHandle closeHelpWindowUsingAccelerator() { + useAcceleratorToCloseHelpWindow(); + return new HelpWindowHandle(guiRobot, primaryStage); + } + + public HelpWindowHandle toggleHelpWindow() { + toggleBetweenHelpWindowAndMainWindow(); + return new HelpWindowHandle(guiRobot, primaryStage); + } + + private void useMenuItemToOpenHelpWindow() { + clickOn(HELP, HELP_MENU_ITEM); + } + + private void useAcceleratorToOpenHelpWindow() { + guiRobot.push(new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN)); guiRobot.sleep(500); } + + private void useAcceleratorToCloseHelpWindow() { + guiRobot.push(new KeyCodeCombination(KeyCode.ESCAPE)); + guiRobot.sleep(500); + } + + private void toggleBetweenHelpWindowAndMainWindow() { + KeyCodeCombination toggle = new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN); + guiRobot.push(toggle); + guiRobot.sleep(500); + guiRobot.push(toggle); + guiRobot.sleep(500); + } + } diff --git a/src/test/java/guitests/guihandles/MessageDisplayHandle.java b/src/test/java/guitests/guihandles/MessageDisplayHandle.java new file mode 100644 index 000000000000..d58d1b0a36c8 --- /dev/null +++ b/src/test/java/guitests/guihandles/MessageDisplayHandle.java @@ -0,0 +1,35 @@ +package guitests.guihandles; + +import guitests.GuiRobot; +import javafx.scene.control.Label; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import seedu.agendum.TestApp; + +//@@author A0148031R +/** + * Handler for the message placeholder of the ui + */ +public class MessageDisplayHandle extends GuiHandle{ + + public static final String MESSAGE_PLACEHOLDER_ID = "#messagePlaceHolder"; + + public MessageDisplayHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public String getText() { + return getMessageDisplay() == null ? null : getMessageDisplay().getText(); + } + + private Label getMessageDisplay() { + try { + StackPane messagePlaceHolder = (StackPane)getNode(MESSAGE_PLACEHOLDER_ID); + return (Label) messagePlaceHolder.getChildren().get(0); + } catch (IllegalStateException e) { + return null; + } catch (IndexOutOfBoundsException e) { + return null; + } + } +} diff --git a/src/test/java/guitests/guihandles/PersonCardHandle.java b/src/test/java/guitests/guihandles/PersonCardHandle.java deleted file mode 100644 index fae22a45ae2f..000000000000 --- a/src/test/java/guitests/guihandles/PersonCardHandle.java +++ /dev/null @@ -1,63 +0,0 @@ -package guitests.guihandles; - -import guitests.GuiRobot; -import javafx.scene.Node; -import javafx.stage.Stage; -import seedu.address.model.person.ReadOnlyPerson; - -/** - * Provides a handle to a person card in the person list panel. - */ -public class PersonCardHandle extends GuiHandle { - private static final String NAME_FIELD_ID = "#name"; - private static final String ADDRESS_FIELD_ID = "#address"; - private static final String PHONE_FIELD_ID = "#phone"; - private static final String EMAIL_FIELD_ID = "#email"; - - private Node node; - - public PersonCardHandle(GuiRobot guiRobot, Stage primaryStage, Node node){ - super(guiRobot, primaryStage, null); - this.node = node; - } - - protected String getTextFromLabel(String fieldId) { - return getTextFromLabel(fieldId, node); - } - - public String getFullName() { - return getTextFromLabel(NAME_FIELD_ID); - } - - public String getAddress() { - return getTextFromLabel(ADDRESS_FIELD_ID); - } - - public String getPhone() { - return getTextFromLabel(PHONE_FIELD_ID); - } - - public String getEmail() { - return getTextFromLabel(EMAIL_FIELD_ID); - } - - public boolean isSamePerson(ReadOnlyPerson person){ - return getFullName().equals(person.getName().fullName) && getPhone().equals(person.getPhone().value) - && getEmail().equals(person.getEmail().value) && getAddress().equals(person.getAddress().value); - } - - @Override - public boolean equals(Object obj) { - if(obj instanceof PersonCardHandle) { - PersonCardHandle handle = (PersonCardHandle) obj; - return getFullName().equals(handle.getFullName()) - && getAddress().equals(handle.getAddress()); //TODO: compare the rest - } - return super.equals(obj); - } - - @Override - public String toString() { - return getFullName() + " " + getAddress(); - } -} diff --git a/src/test/java/guitests/guihandles/PersonListPanelHandle.java b/src/test/java/guitests/guihandles/PersonListPanelHandle.java deleted file mode 100644 index 3451992cf735..000000000000 --- a/src/test/java/guitests/guihandles/PersonListPanelHandle.java +++ /dev/null @@ -1,172 +0,0 @@ -package guitests.guihandles; - - -import guitests.GuiRobot; -import javafx.geometry.Point2D; -import javafx.scene.Node; -import javafx.scene.control.ListView; -import javafx.stage.Stage; -import seedu.address.TestApp; -import seedu.address.model.person.Person; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.testutil.TestUtil; - -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static org.junit.Assert.assertTrue; - -/** - * Provides a handle for the panel containing the person list. - */ -public class PersonListPanelHandle extends GuiHandle { - - public static final int NOT_FOUND = -1; - public static final String CARD_PANE_ID = "#cardPane"; - - private static final String PERSON_LIST_VIEW_ID = "#personListView"; - - public PersonListPanelHandle(GuiRobot guiRobot, Stage primaryStage) { - super(guiRobot, primaryStage, TestApp.APP_TITLE); - } - - public List getSelectedPersons() { - ListView personList = getListView(); - return personList.getSelectionModel().getSelectedItems(); - } - - public ListView getListView() { - return (ListView) getNode(PERSON_LIST_VIEW_ID); - } - - /** - * Returns true if the list is showing the person details correctly and in correct order. - * @param persons A list of person in the correct order. - */ - public boolean isListMatching(ReadOnlyPerson... persons) { - return this.isListMatching(0, persons); - } - - /** - * Clicks on the ListView. - */ - public void clickOnListView() { - Point2D point= TestUtil.getScreenMidPoint(getListView()); - guiRobot.clickOn(point.getX(), point.getY()); - } - - /** - * Returns true if the {@code persons} appear as the sub list (in that order) at position {@code startPosition}. - */ - public boolean containsInOrder(int startPosition, ReadOnlyPerson... persons) { - List personsInList = getListView().getItems(); - - // Return false if the list in panel is too short to contain the given list - if (startPosition + persons.length > personsInList.size()){ - return false; - } - - // Return false if any of the persons doesn't match - for (int i = 0; i < persons.length; i++) { - if (!personsInList.get(startPosition + i).getName().fullName.equals(persons[i].getName().fullName)){ - return false; - } - } - - return true; - } - - /** - * Returns true if the list is showing the person details correctly and in correct order. - * @param startPosition The starting position of the sub list. - * @param persons A list of person in the correct order. - */ - public boolean isListMatching(int startPosition, ReadOnlyPerson... persons) throws IllegalArgumentException { - if (persons.length + startPosition != getListView().getItems().size()) { - throw new IllegalArgumentException("List size mismatched\n" + - "Expected " + (getListView().getItems().size() - 1) + " persons"); - } - assertTrue(this.containsInOrder(startPosition, persons)); - for (int i = 0; i < persons.length; i++) { - final int scrollTo = i + startPosition; - guiRobot.interact(() -> getListView().scrollTo(scrollTo)); - guiRobot.sleep(200); - if (!TestUtil.compareCardAndPerson(getPersonCardHandle(startPosition + i), persons[i])) { - return false; - } - } - return true; - } - - - public PersonCardHandle navigateToPerson(String name) { - guiRobot.sleep(500); //Allow a bit of time for the list to be updated - final Optional person = getListView().getItems().stream().filter(p -> p.getName().fullName.equals(name)).findAny(); - if (!person.isPresent()) { - throw new IllegalStateException("Name not found: " + name); - } - - return navigateToPerson(person.get()); - } - - /** - * Navigates the listview to display and select the person. - */ - public PersonCardHandle navigateToPerson(ReadOnlyPerson person) { - int index = getPersonIndex(person); - - guiRobot.interact(() -> { - getListView().scrollTo(index); - guiRobot.sleep(150); - getListView().getSelectionModel().select(index); - }); - guiRobot.sleep(100); - return getPersonCardHandle(person); - } - - - /** - * Returns the position of the person given, {@code NOT_FOUND} if not found in the list. - */ - public int getPersonIndex(ReadOnlyPerson targetPerson) { - List personsInList = getListView().getItems(); - for (int i = 0; i < personsInList.size(); i++) { - if(personsInList.get(i).getName().equals(targetPerson.getName())){ - return i; - } - } - return NOT_FOUND; - } - - /** - * Gets a person from the list by index - */ - public ReadOnlyPerson getPerson(int index) { - return getListView().getItems().get(index); - } - - public PersonCardHandle getPersonCardHandle(int index) { - return getPersonCardHandle(new Person(getListView().getItems().get(index))); - } - - public PersonCardHandle getPersonCardHandle(ReadOnlyPerson person) { - Set nodes = getAllCardNodes(); - Optional personCardNode = nodes.stream() - .filter(n -> new PersonCardHandle(guiRobot, primaryStage, n).isSamePerson(person)) - .findFirst(); - if (personCardNode.isPresent()) { - return new PersonCardHandle(guiRobot, primaryStage, personCardNode.get()); - } else { - return null; - } - } - - protected Set getAllCardNodes() { - return guiRobot.lookup(CARD_PANE_ID).queryAll(); - } - - public int getNumberOfPeople() { - return getListView().getItems().size(); - } -} diff --git a/src/test/java/guitests/guihandles/ResultDisplayHandle.java b/src/test/java/guitests/guihandles/ResultDisplayHandle.java index 110b4682b184..adb957b7d16f 100644 --- a/src/test/java/guitests/guihandles/ResultDisplayHandle.java +++ b/src/test/java/guitests/guihandles/ResultDisplayHandle.java @@ -1,10 +1,11 @@ package guitests.guihandles; import guitests.GuiRobot; -import javafx.scene.control.TextArea; +import javafx.scene.control.Label; import javafx.stage.Stage; -import seedu.address.TestApp; +import seedu.agendum.TestApp; +//@@author A0148031R /** * A handler for the ResultDisplay of the UI */ @@ -20,7 +21,7 @@ public String getText() { return getResultDisplay().getText(); } - private TextArea getResultDisplay() { - return (TextArea) getNode(RESULT_DISPLAY_ID); + private Label getResultDisplay() { + return (Label) getNode(RESULT_DISPLAY_ID); } } diff --git a/src/test/java/guitests/guihandles/TaskCardHandle.java b/src/test/java/guitests/guihandles/TaskCardHandle.java new file mode 100644 index 000000000000..3830b4a4ad19 --- /dev/null +++ b/src/test/java/guitests/guihandles/TaskCardHandle.java @@ -0,0 +1,144 @@ +package guitests.guihandles; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import guitests.GuiRobot; +import javafx.scene.Node; +import javafx.stage.Stage; +import seedu.agendum.model.task.ReadOnlyTask; + +//@@author A0148031R +/** + * Provides a handle to a task card in the task list panel. + */ +public class TaskCardHandle extends GuiHandle { + private static final String NAME_FIELD_ID = "#name"; + private static final String INDEX_FIELD_ID = "#id"; + private static final String TIME_FIELD_ID = "#time"; + private static final String TASK_TIME_PATTERN = "HH:mm EEE, dd MMM"; + private static final String COMPLETED_TIME_PATTERN = "EEE, dd MMM"; + private static final String OVERDUE_PREFIX = "Overdue\n"; + private static final String COMPLETED_PREFIX = "Completed on "; + private static final String START_TIME_PREFIX = "from "; + private static final String END_TIME_PREFIX = " to "; + private static final String DEADLINE_PREFIX = "by "; + private static final String EMPTY_PREFIX = ""; + + private Node node; + + public TaskCardHandle(GuiRobot guiRobot, Stage primaryStage, Node node) { + super(guiRobot, primaryStage, null); + this.node = node; + } + + protected String getTextFromLabel(String fieldId) { + return getTextFromLabel(fieldId, node); + } + + public String getName() { + return getTextFromLabel(NAME_FIELD_ID); + } + + public String getTaskIndex() { + return getTextFromLabel(INDEX_FIELD_ID); + } + + public String getTime() { + return getTextFromLabel(TIME_FIELD_ID); + } + + public boolean isSameTask(ReadOnlyTask task) { + + String name = task.getName().fullName; + + if (!task.isCompleted() && !task.hasTime()) { + return getName().equals(name); + } + + StringBuilder timeDescription = new StringBuilder(); + timeDescription.append(formatTaskTime(task)); + + if (task.isCompleted()) { + timeDescription.append(formatUpdatedTime(task)); + } + + return getName().equals(name) && getTime().equals(timeDescription.toString()); + } + + public String formatTime(String dateTimePattern, String prefix, Optional dateTime) { + + StringBuilder sb = new StringBuilder(); + DateTimeFormatter format = DateTimeFormatter.ofPattern(dateTimePattern); + sb.append(prefix).append(dateTime.get().format(format)); + + return sb.toString(); + } + + public String formatTaskTime(ReadOnlyTask task) { + + StringBuilder timeStringBuilder = new StringBuilder(); + + if (task.isOverdue()) { + timeStringBuilder.append(OVERDUE_PREFIX); + } + + if (task.isEvent()) { + String startTime = formatTime(TASK_TIME_PATTERN, START_TIME_PREFIX, task.getStartDateTime()); + String endTime = formatTime(TASK_TIME_PATTERN, END_TIME_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(startTime); + timeStringBuilder.append(endTime); + } else if (task.hasDeadline()) { + String deadline = formatTime(TASK_TIME_PATTERN, DEADLINE_PREFIX, task.getEndDateTime()); + timeStringBuilder.append(deadline); + } + + return timeStringBuilder.toString(); + } + + public String formatUpdatedTime(ReadOnlyTask task) { + StringBuilder timeStringBuilder = new StringBuilder(); + if (task.hasTime()) { + timeStringBuilder.append("\n"); + } + timeStringBuilder.append(COMPLETED_PREFIX); + timeStringBuilder.append(formatTime(COMPLETED_TIME_PATTERN, EMPTY_PREFIX, + Optional.ofNullable(task.getLastUpdatedTime()))); + return timeStringBuilder.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TaskCardHandle) { + TaskCardHandle handle = (TaskCardHandle) obj; + return getName().equals(handle.getName()) && getTaskIndex().equals(handle.getTaskIndex()) + && getTime().equals(handle.getTime()); + } + return super.equals(obj); + } + + @Override + public String toString() { + return getTaskIndex() + " " + getName() + "Time: " + getTime(); + } + + public String formatTime(ReadOnlyTask task, String dateTimePattern, String prefix, + Optional dateTime) { + + StringBuilder sb = new StringBuilder(); + DateTimeFormatter format = DateTimeFormatter.ofPattern(dateTimePattern); + + if (task.isCompleted()) { + sb.append(dateTime.get().format(format)); + } else if (dateTime.isPresent() && task.getStartDateTime().isPresent()) { + sb.append(prefix).append(dateTime.get().format(format)); + } else if (dateTime.isPresent()) { + sb.append(DEADLINE_PREFIX).append(dateTime.get().format(format)); + } else { + sb.append(EMPTY_PREFIX); + } + + return sb.toString().toLowerCase(); + } +} diff --git a/src/test/java/guitests/guihandles/UpcomingTasksHandle.java b/src/test/java/guitests/guihandles/UpcomingTasksHandle.java new file mode 100644 index 000000000000..c624ec8412dc --- /dev/null +++ b/src/test/java/guitests/guihandles/UpcomingTasksHandle.java @@ -0,0 +1,169 @@ +package guitests.guihandles; + + +import guitests.GuiRobot; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ListView; +import javafx.stage.Stage; +import seedu.agendum.TestApp; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; +import seedu.agendum.testutil.TestUtil; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.assertTrue; + +/** + * Provides a handle for the panel containing the task list. + */ +public class UpcomingTasksHandle extends GuiHandle { + + public static final int NOT_FOUND = -1; + public static final String CARD_PANE_ID = "#cardPane"; + + private static final String TASK_LIST_VIEW_ID = "#upcomingTasksListView"; + + public UpcomingTasksHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + public List getSelectedTasks() { + ListView taskList = getListView(); + return taskList.getSelectionModel().getSelectedItems(); + } + + public ListView getListView() { + return (ListView) getNode(TASK_LIST_VIEW_ID); + } + + /** + * Returns true if the list is showing the task details correctly and in correct order. + * @param tasks A list of tasks in the correct order. + */ + public boolean isListMatching(ReadOnlyTask... tasks) { + return this.isListMatching(0, tasks); + } + + public void clickOnListView() { + Point2D point= TestUtil.getScreenMidPoint(getListView()); + guiRobot.clickOn(point.getX(), point.getY()); + } + + /** + * Returns true if the {@code tasks} appear as the sub list (in that order) at position {@code startPosition}. + */ + public boolean containsInOrder(int startPosition, ReadOnlyTask... tasks) { + List tasksInList = getListView().getItems(); + + // Return false if the list in panel is too short to contain the given list + if (startPosition + tasks.length > tasksInList.size()){ + return false; + } + + // Return false if any of the tasks doesn't match + for (int i = 0; i < tasks.length; i++) { + if (!tasksInList.get(startPosition + i).getName().fullName.equals(tasks[i].getName().fullName)){ + return false; + } + } + + return true; + } + + /** + * Returns true if the list is showing the task details correctly and in correct order. + * @param startPosition The starting position of the sub list. + * @param tasks A list of tasks in the correct order. + */ + public boolean isListMatching(int startPosition, ReadOnlyTask... tasks) throws IllegalArgumentException { + if (tasks.length + startPosition != getListView().getItems().size()) { + throw new IllegalArgumentException("List size mismatched\n" + + "Expected " + (getListView().getItems().size() - 1) + " tasks"); + } + assertTrue(this.containsInOrder(startPosition, tasks)); + for (int i = 0; i < tasks.length; i++) { + final int scrollTo = i + startPosition; + guiRobot.interact(() -> getListView().scrollTo(scrollTo)); + guiRobot.sleep(200); + if (!TestUtil.compareCardAndTask(getTaskCardHandle(startPosition + i), tasks[i])) { + return false; + } + } + return true; + } + + + public TaskCardHandle navigateToTask(String name) { + guiRobot.sleep(500); //Allow a bit of time for the list to be updated + final Optional task = getListView().getItems().stream().filter(p -> p.getName().fullName.equals(name)).findAny(); + if (!task.isPresent()) { + throw new IllegalStateException("Name not found: " + name); + } + + return navigateToTask(task.get()); + } + + /** + * Navigates the listview to display and select the task. + */ + public TaskCardHandle navigateToTask(ReadOnlyTask task) { + int index = getTaskIndex(task); + + guiRobot.interact(() -> { + getListView().scrollTo(index); + guiRobot.sleep(150); + getListView().getSelectionModel().select(index); + }); + guiRobot.sleep(100); + return getTaskCardHandle(task); + } + + + /** + * Returns the position of the task given, {@code NOT_FOUND} if not found in the list. + */ + public int getTaskIndex(ReadOnlyTask targetTask) { + List tasksInList = getListView().getItems(); + for (int i = 0; i < tasksInList.size(); i++) { + if(tasksInList.get(i).getName().equals(targetTask.getName())){ + return i; + } + } + return NOT_FOUND; + } + + /** + * Gets a task from the list by index + */ + public ReadOnlyTask getTask(int index) { + return getListView().getItems().get(index); + } + + public TaskCardHandle getTaskCardHandle(int index) { + return getTaskCardHandle(new Task(getListView().getItems().get(index))); + } + + public TaskCardHandle getTaskCardHandle(ReadOnlyTask task) { + Set nodes = getAllCardNodes(); + Optional taskCardNode = nodes.stream() + .filter(n -> new TaskCardHandle(guiRobot, primaryStage, n).isSameTask(task)) + .findFirst(); + if (taskCardNode.isPresent()) { + return new TaskCardHandle(guiRobot, primaryStage, taskCardNode.get()); + } else { + return null; + } + } + + protected Set getAllCardNodes() { + return guiRobot.lookup(CARD_PANE_ID).queryAll(); + } + + public int getNumberOfTasks() { + return getListView().getItems().size(); + } +} diff --git a/src/test/java/seedu/address/commons/core/ConfigTest.java b/src/test/java/seedu/address/commons/core/ConfigTest.java deleted file mode 100644 index 62d58646f736..000000000000 --- a/src/test/java/seedu/address/commons/core/ConfigTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package seedu.address.commons.core; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ConfigTest { - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void toString_defaultObject_stringReturned() { - String defaultConfigAsString = "App title : Address App\n" + - "Current log level : INFO\n" + - "Preference file Location : preferences.json\n" + - "Local data file location : data/addressbook.xml\n" + - "AddressBook name : MyAddressBook"; - - assertEquals(defaultConfigAsString, new Config().toString()); - } - - @Test - public void equalsMethod(){ - Config defaultConfig = new Config(); - assertFalse(defaultConfig.equals(null)); - assertTrue(defaultConfig.equals(defaultConfig)); - } - - -} diff --git a/src/test/java/seedu/address/commons/util/AppUtilTest.java b/src/test/java/seedu/address/commons/util/AppUtilTest.java deleted file mode 100644 index fbea1d0c1e8e..000000000000 --- a/src/test/java/seedu/address/commons/util/AppUtilTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package seedu.address.commons.util; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.junit.Assert.assertNotNull; - -public class AppUtilTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - - - @Test - public void getImage_exitingImage(){ - assertNotNull(AppUtil.getImage("/images/address_book_32.png")); - } - - - @Test - public void getImage_nullGiven_assertionError(){ - thrown.expect(AssertionError.class); - AppUtil.getImage(null); - } - - -} diff --git a/src/test/java/seedu/address/commons/util/FileUtilTest.java b/src/test/java/seedu/address/commons/util/FileUtilTest.java deleted file mode 100644 index 8de2621799cf..000000000000 --- a/src/test/java/seedu/address/commons/util/FileUtilTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package seedu.address.commons.util; - - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import seedu.address.testutil.SerializableTestClass; -import seedu.address.testutil.TestUtil; - -import java.io.File; -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -public class FileUtilTest { - private static final File SERIALIZATION_FILE = new File(TestUtil.getFilePathInSandboxFolder("serialize.json")); - - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void getPath(){ - - // valid case - assertEquals("folder" + File.separator + "sub-folder", FileUtil.getPath("folder/sub-folder")); - - // null parameter -> assertion failure - thrown.expect(AssertionError.class); - FileUtil.getPath(null); - - // no forwards slash -> assertion failure - thrown.expect(AssertionError.class); - FileUtil.getPath("folder"); - } - - @Test - public void serializeObjectToJsonFile_noExceptionThrown() throws IOException { - SerializableTestClass serializableTestClass = new SerializableTestClass(); - serializableTestClass.setTestValues(); - - FileUtil.serializeObjectToJsonFile(SERIALIZATION_FILE, serializableTestClass); - - assertEquals(FileUtil.readFromFile(SERIALIZATION_FILE), SerializableTestClass.JSON_STRING_REPRESENTATION); - } - - @Test - public void deserializeObjectFromJsonFile_noExceptionThrown() throws IOException { - FileUtil.writeToFile(SERIALIZATION_FILE, SerializableTestClass.JSON_STRING_REPRESENTATION); - - SerializableTestClass serializableTestClass = FileUtil - .deserializeObjectFromJsonFile(SERIALIZATION_FILE, SerializableTestClass.class); - - assertEquals(serializableTestClass.getName(), SerializableTestClass.getNameTestValue()); - assertEquals(serializableTestClass.getListOfLocalDateTimes(), SerializableTestClass.getListTestValues()); - assertEquals(serializableTestClass.getMapOfIntegerToString(), SerializableTestClass.getHashMapTestValues()); - } -} diff --git a/src/test/java/seedu/address/commons/util/StringUtilTest.java b/src/test/java/seedu/address/commons/util/StringUtilTest.java deleted file mode 100644 index 194dd71d2c3f..000000000000 --- a/src/test/java/seedu/address/commons/util/StringUtilTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package seedu.address.commons.util; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import java.io.FileNotFoundException; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class StringUtilTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void isUnsignedPositiveInteger() { - assertFalse(StringUtil.isUnsignedInteger(null)); - assertFalse(StringUtil.isUnsignedInteger("")); - assertFalse(StringUtil.isUnsignedInteger("a")); - assertFalse(StringUtil.isUnsignedInteger("aaa")); - assertFalse(StringUtil.isUnsignedInteger(" ")); - assertFalse(StringUtil.isUnsignedInteger("-1")); - assertFalse(StringUtil.isUnsignedInteger("0")); - assertFalse(StringUtil.isUnsignedInteger("+1")); //should be unsigned - assertFalse(StringUtil.isUnsignedInteger("-1")); //should be unsigned - assertFalse(StringUtil.isUnsignedInteger(" 10")); //should not contain whitespaces - assertFalse(StringUtil.isUnsignedInteger("10 ")); //should not contain whitespaces - assertFalse(StringUtil.isUnsignedInteger("1 0")); //should not contain whitespaces - - assertTrue(StringUtil.isUnsignedInteger("1")); - assertTrue(StringUtil.isUnsignedInteger("10")); - } - - @Test - public void getDetails_exceptionGiven(){ - assertThat(StringUtil.getDetails(new FileNotFoundException("file not found")), - containsString("java.io.FileNotFoundException: file not found")); - } - - @Test - public void getDetails_nullGiven_assertionError(){ - thrown.expect(AssertionError.class); - StringUtil.getDetails(null); - } - - -} diff --git a/src/test/java/seedu/address/commons/util/UrlUtilTest.java b/src/test/java/seedu/address/commons/util/UrlUtilTest.java deleted file mode 100644 index 58efab5fd499..000000000000 --- a/src/test/java/seedu/address/commons/util/UrlUtilTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package seedu.address.commons.util; - -import org.junit.Test; - -import java.net.MalformedURLException; -import java.net.URL; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * Tests the UrlUtil methods. - */ -public class UrlUtilTest { - - @Test - public void compareBaseUrls_differentCapital_success() throws MalformedURLException { - URL url1 = new URL("https://www.Google.com/a"); - URL url2 = new URL("https://www.google.com/A"); - assertTrue(UrlUtil.compareBaseUrls(url1, url2)); - } - - @Test - public void compareBaseUrls_testWithAndWithoutWww_success() throws MalformedURLException { - URL url1 = new URL("https://google.com/a"); - URL url2 = new URL("https://www.google.com/a"); - assertTrue(UrlUtil.compareBaseUrls(url1, url2)); - } - - @Test - public void compareBaseUrls_differentSlashes_success() throws MalformedURLException { - URL url1 = new URL("https://www.Google.com/a/acb/"); - URL url2 = new URL("https://www.google.com/A/acb"); - assertTrue(UrlUtil.compareBaseUrls(url1, url2)); - } - - @Test - public void compareBaseUrls_differentUrl_fail() throws MalformedURLException { - URL url1 = new URL("https://www.Google.com/a/ac_b/"); - URL url2 = new URL("https://www.google.com/A/acb"); - assertFalse(UrlUtil.compareBaseUrls(url1, url2)); - } - - @Test - public void compareBaseUrls_null_false() throws MalformedURLException { - URL url1 = new URL("https://www.Google.com/a/ac_b/"); - URL url2 = new URL("https://www.google.com/A/acb"); - assertFalse(UrlUtil.compareBaseUrls(url1, null)); - assertFalse(UrlUtil.compareBaseUrls(null, url2)); - assertFalse(UrlUtil.compareBaseUrls(null, null)); - } -} diff --git a/src/test/java/seedu/address/commons/util/XmlUtilTest.java b/src/test/java/seedu/address/commons/util/XmlUtilTest.java deleted file mode 100644 index dc4fd886c23e..000000000000 --- a/src/test/java/seedu/address/commons/util/XmlUtilTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package seedu.address.commons.util; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import seedu.address.model.AddressBook; -import seedu.address.storage.XmlSerializableAddressBook; -import seedu.address.testutil.AddressBookBuilder; -import seedu.address.testutil.TestUtil; - -import javax.xml.bind.JAXBException; -import java.io.File; -import java.io.FileNotFoundException; - -import static org.junit.Assert.assertEquals; - -public class XmlUtilTest { - - private static final String TEST_DATA_FOLDER = FileUtil.getPath("src/test/data/XmlUtilTest/"); - private static final File EMPTY_FILE = new File(TEST_DATA_FOLDER + "empty.xml"); - private static final File MISSING_FILE = new File(TEST_DATA_FOLDER + "missing.xml"); - private static final File VALID_FILE = new File(TEST_DATA_FOLDER + "validAddressBook.xml"); - private static final File TEMP_FILE = new File(TestUtil.getFilePathInSandboxFolder("tempAddressBook.xml")); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void getDataFromFile_nullFile_AssertionError() throws Exception { - thrown.expect(AssertionError.class); - XmlUtil.getDataFromFile(null, AddressBook.class); - } - - @Test - public void getDataFromFile_nullClass_AssertionError() throws Exception { - thrown.expect(AssertionError.class); - XmlUtil.getDataFromFile(VALID_FILE, null); - } - - @Test - public void getDataFromFile_missingFile_FileNotFoundException() throws Exception { - thrown.expect(FileNotFoundException.class); - XmlUtil.getDataFromFile(MISSING_FILE, AddressBook.class); - } - - @Test - public void getDataFromFile_emptyFile_DataFormatMismatchException() throws Exception { - thrown.expect(JAXBException.class); - XmlUtil.getDataFromFile(EMPTY_FILE, AddressBook.class); - } - - @Test - public void getDataFromFile_validFile_validResult() throws Exception { - XmlSerializableAddressBook dataFromFile = XmlUtil.getDataFromFile(VALID_FILE, XmlSerializableAddressBook.class); - assertEquals(9, dataFromFile.getPersonList().size()); - assertEquals(0, dataFromFile.getTagList().size()); - } - - @Test - public void saveDataToFile_nullFile_AssertionError() throws Exception { - thrown.expect(AssertionError.class); - XmlUtil.saveDataToFile(null, new AddressBook()); - } - - @Test - public void saveDataToFile_nullClass_AssertionError() throws Exception { - thrown.expect(AssertionError.class); - XmlUtil.saveDataToFile(VALID_FILE, null); - } - - @Test - public void saveDataToFile_missingFile_FileNotFoundException() throws Exception { - thrown.expect(FileNotFoundException.class); - XmlUtil.saveDataToFile(MISSING_FILE, new AddressBook()); - } - - @Test - public void saveDataToFile_validFile_dataSaved() throws Exception { - TEMP_FILE.createNewFile(); - XmlSerializableAddressBook dataToWrite = new XmlSerializableAddressBook(new AddressBook()); - XmlUtil.saveDataToFile(TEMP_FILE, dataToWrite); - XmlSerializableAddressBook dataFromFile = XmlUtil.getDataFromFile(TEMP_FILE, XmlSerializableAddressBook.class); - assertEquals((new AddressBook(dataToWrite)).toString(),(new AddressBook(dataFromFile)).toString()); - //TODO: use equality instead of string comparisons - - AddressBookBuilder builder = new AddressBookBuilder(new AddressBook()); - dataToWrite = new XmlSerializableAddressBook(builder.withPerson(TestUtil.generateSamplePersonData().get(0)).withTag("Friends").build()); - - XmlUtil.saveDataToFile(TEMP_FILE, dataToWrite); - dataFromFile = XmlUtil.getDataFromFile(TEMP_FILE, XmlSerializableAddressBook.class); - assertEquals((new AddressBook(dataToWrite)).toString(),(new AddressBook(dataFromFile)).toString()); - } -} diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java deleted file mode 100644 index e1ee0cfb4051..000000000000 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ /dev/null @@ -1,512 +0,0 @@ -package seedu.address.logic; - -import com.google.common.eventbus.Subscribe; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import seedu.address.commons.core.EventsCenter; -import seedu.address.logic.commands.*; -import seedu.address.commons.events.ui.JumpToListRequestEvent; -import seedu.address.commons.events.ui.ShowHelpRequestEvent; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.*; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; -import seedu.address.storage.StorageManager; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static seedu.address.commons.core.Messages.*; - -public class LogicManagerTest { - - /** - * See https://github.com/junit-team/junit4/wiki/rules#temporaryfolder-rule - */ - @Rule - public TemporaryFolder saveFolder = new TemporaryFolder(); - - private Model model; - private Logic logic; - - //These are for checking the correctness of the events raised - private ReadOnlyAddressBook latestSavedAddressBook; - private boolean helpShown; - private int targetedJumpIndex; - - @Subscribe - private void handleLocalModelChangedEvent(AddressBookChangedEvent abce) { - latestSavedAddressBook = new AddressBook(abce.data); - } - - @Subscribe - private void handleShowHelpRequestEvent(ShowHelpRequestEvent she) { - helpShown = true; - } - - @Subscribe - private void handleJumpToListRequestEvent(JumpToListRequestEvent je) { - targetedJumpIndex = je.targetIndex; - } - - @Before - public void setup() { - model = new ModelManager(); - String tempAddressBookFile = saveFolder.getRoot().getPath() + "TempAddressBook.xml"; - String tempPreferencesFile = saveFolder.getRoot().getPath() + "TempPreferences.json"; - logic = new LogicManager(model, new StorageManager(tempAddressBookFile, tempPreferencesFile)); - EventsCenter.getInstance().registerHandler(this); - - latestSavedAddressBook = new AddressBook(model.getAddressBook()); // last saved assumed to be up to date before. - helpShown = false; - targetedJumpIndex = -1; // non yet - } - - @After - public void teardown() { - EventsCenter.clearSubscribers(); - } - - @Test - public void execute_invalid() throws Exception { - String invalidCommand = " "; - assertCommandBehavior(invalidCommand, - String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - /** - * Executes the command and confirms that the result message is correct. - * Both the 'address book' and the 'last shown list' are expected to be empty. - * @see #assertCommandBehavior(String, String, ReadOnlyAddressBook, List) - */ - private void assertCommandBehavior(String inputCommand, String expectedMessage) throws Exception { - assertCommandBehavior(inputCommand, expectedMessage, new AddressBook(), Collections.emptyList()); - } - - /** - * Executes the command and confirms that the result message is correct and - * also confirms that the following three parts of the LogicManager object's state are as expected:
- * - the internal address book data are same as those in the {@code expectedAddressBook}
- * - the backing list shown by UI matches the {@code shownList}
- * - {@code expectedAddressBook} was saved to the storage file.
- */ - private void assertCommandBehavior(String inputCommand, String expectedMessage, - ReadOnlyAddressBook expectedAddressBook, - List expectedShownList) throws Exception { - - //Execute the command - CommandResult result = logic.execute(inputCommand); - - //Confirm the ui display elements should contain the right data - assertEquals(expectedMessage, result.feedbackToUser); - assertEquals(expectedShownList, model.getFilteredPersonList()); - - //Confirm the state of data (saved and in-memory) is as expected - assertEquals(expectedAddressBook, model.getAddressBook()); - assertEquals(expectedAddressBook, latestSavedAddressBook); - } - - - @Test - public void execute_unknownCommandWord() throws Exception { - String unknownCommand = "uicfhmowqewca"; - assertCommandBehavior(unknownCommand, MESSAGE_UNKNOWN_COMMAND); - } - - @Test - public void execute_help() throws Exception { - assertCommandBehavior("help", HelpCommand.SHOWING_HELP_MESSAGE); - assertTrue(helpShown); - } - - @Test - public void execute_exit() throws Exception { - assertCommandBehavior("exit", ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT); - } - - @Test - public void execute_clear() throws Exception { - TestDataHelper helper = new TestDataHelper(); - model.addPerson(helper.generatePerson(1)); - model.addPerson(helper.generatePerson(2)); - model.addPerson(helper.generatePerson(3)); - - assertCommandBehavior("clear", ClearCommand.MESSAGE_SUCCESS, new AddressBook(), Collections.emptyList()); - } - - - @Test - public void execute_add_invalidArgsFormat() throws Exception { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); - assertCommandBehavior( - "add wrong args wrong args", expectedMessage); - assertCommandBehavior( - "add Valid Name 12345 e/valid@email.butNoPhonePrefix a/valid, address", expectedMessage); - assertCommandBehavior( - "add Valid Name p/12345 valid@email.butNoPrefix a/valid, address", expectedMessage); - assertCommandBehavior( - "add Valid Name p/12345 e/valid@email.butNoAddressPrefix valid, address", expectedMessage); - } - - @Test - public void execute_add_invalidPersonData() throws Exception { - assertCommandBehavior( - "add []\\[;] p/12345 e/valid@e.mail a/valid, address", Name.MESSAGE_NAME_CONSTRAINTS); - assertCommandBehavior( - "add Valid Name p/not_numbers e/valid@e.mail a/valid, address", Phone.MESSAGE_PHONE_CONSTRAINTS); - assertCommandBehavior( - "add Valid Name p/12345 e/notAnEmail a/valid, address", Email.MESSAGE_EMAIL_CONSTRAINTS); - assertCommandBehavior( - "add Valid Name p/12345 e/valid@e.mail a/valid, address t/invalid_-[.tag", Tag.MESSAGE_TAG_CONSTRAINTS); - - } - - @Test - public void execute_add_successful() throws Exception { - // setup expectations - TestDataHelper helper = new TestDataHelper(); - Person toBeAdded = helper.adam(); - AddressBook expectedAB = new AddressBook(); - expectedAB.addPerson(toBeAdded); - - // execute command and verify result - assertCommandBehavior(helper.generateAddCommand(toBeAdded), - String.format(AddCommand.MESSAGE_SUCCESS, toBeAdded), - expectedAB, - expectedAB.getPersonList()); - - } - - @Test - public void execute_addDuplicate_notAllowed() throws Exception { - // setup expectations - TestDataHelper helper = new TestDataHelper(); - Person toBeAdded = helper.adam(); - AddressBook expectedAB = new AddressBook(); - expectedAB.addPerson(toBeAdded); - - // setup starting state - model.addPerson(toBeAdded); // person already in internal address book - - // execute command and verify result - assertCommandBehavior( - helper.generateAddCommand(toBeAdded), - AddCommand.MESSAGE_DUPLICATE_PERSON, - expectedAB, - expectedAB.getPersonList()); - - } - - - @Test - public void execute_list_showsAllPersons() throws Exception { - // prepare expectations - TestDataHelper helper = new TestDataHelper(); - AddressBook expectedAB = helper.generateAddressBook(2); - List expectedList = expectedAB.getPersonList(); - - // prepare address book state - helper.addToModel(model, 2); - - assertCommandBehavior("list", - ListCommand.MESSAGE_SUCCESS, - expectedAB, - expectedList); - } - - - /** - * Confirms the 'invalid argument index number behaviour' for the given command - * targeting a single person in the shown list, using visible index. - * @param commandWord to test assuming it targets a single person in the last shown list based on visible index. - */ - private void assertIncorrectIndexFormatBehaviorForCommand(String commandWord, String expectedMessage) throws Exception { - assertCommandBehavior(commandWord , expectedMessage); //index missing - assertCommandBehavior(commandWord + " +1", expectedMessage); //index should be unsigned - assertCommandBehavior(commandWord + " -1", expectedMessage); //index should be unsigned - assertCommandBehavior(commandWord + " 0", expectedMessage); //index cannot be 0 - assertCommandBehavior(commandWord + " not_a_number", expectedMessage); - } - - /** - * Confirms the 'invalid argument index number behaviour' for the given command - * targeting a single person in the shown list, using visible index. - * @param commandWord to test assuming it targets a single person in the last shown list based on visible index. - */ - private void assertIndexNotFoundBehaviorForCommand(String commandWord) throws Exception { - String expectedMessage = MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; - TestDataHelper helper = new TestDataHelper(); - List personList = helper.generatePersonList(2); - - // set AB state to 2 persons - model.resetData(new AddressBook()); - for (Person p : personList) { - model.addPerson(p); - } - - assertCommandBehavior(commandWord + " 3", expectedMessage, model.getAddressBook(), personList); - } - - @Test - public void execute_selectInvalidArgsFormat_errorMessageShown() throws Exception { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE); - assertIncorrectIndexFormatBehaviorForCommand("select", expectedMessage); - } - - @Test - public void execute_selectIndexNotFound_errorMessageShown() throws Exception { - assertIndexNotFoundBehaviorForCommand("select"); - } - - @Test - public void execute_select_jumpsToCorrectPerson() throws Exception { - TestDataHelper helper = new TestDataHelper(); - List threePersons = helper.generatePersonList(3); - - AddressBook expectedAB = helper.generateAddressBook(threePersons); - helper.addToModel(model, threePersons); - - assertCommandBehavior("select 2", - String.format(SelectCommand.MESSAGE_SELECT_PERSON_SUCCESS, 2), - expectedAB, - expectedAB.getPersonList()); - assertEquals(1, targetedJumpIndex); - assertEquals(model.getFilteredPersonList().get(1), threePersons.get(1)); - } - - - @Test - public void execute_deleteInvalidArgsFormat_errorMessageShown() throws Exception { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); - assertIncorrectIndexFormatBehaviorForCommand("delete", expectedMessage); - } - - @Test - public void execute_deleteIndexNotFound_errorMessageShown() throws Exception { - assertIndexNotFoundBehaviorForCommand("delete"); - } - - @Test - public void execute_delete_removesCorrectPerson() throws Exception { - TestDataHelper helper = new TestDataHelper(); - List threePersons = helper.generatePersonList(3); - - AddressBook expectedAB = helper.generateAddressBook(threePersons); - expectedAB.removePerson(threePersons.get(1)); - helper.addToModel(model, threePersons); - - assertCommandBehavior("delete 2", - String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, threePersons.get(1)), - expectedAB, - expectedAB.getPersonList()); - } - - - @Test - public void execute_find_invalidArgsFormat() throws Exception { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE); - assertCommandBehavior("find ", expectedMessage); - } - - @Test - public void execute_find_onlyMatchesFullWordsInNames() throws Exception { - TestDataHelper helper = new TestDataHelper(); - Person pTarget1 = helper.generatePersonWithName("bla bla KEY bla"); - Person pTarget2 = helper.generatePersonWithName("bla KEY bla bceofeia"); - Person p1 = helper.generatePersonWithName("KE Y"); - Person p2 = helper.generatePersonWithName("KEYKEYKEY sduauo"); - - List fourPersons = helper.generatePersonList(p1, pTarget1, p2, pTarget2); - AddressBook expectedAB = helper.generateAddressBook(fourPersons); - List expectedList = helper.generatePersonList(pTarget1, pTarget2); - helper.addToModel(model, fourPersons); - - assertCommandBehavior("find KEY", - Command.getMessageForPersonListShownSummary(expectedList.size()), - expectedAB, - expectedList); - } - - @Test - public void execute_find_isNotCaseSensitive() throws Exception { - TestDataHelper helper = new TestDataHelper(); - Person p1 = helper.generatePersonWithName("bla bla KEY bla"); - Person p2 = helper.generatePersonWithName("bla KEY bla bceofeia"); - Person p3 = helper.generatePersonWithName("key key"); - Person p4 = helper.generatePersonWithName("KEy sduauo"); - - List fourPersons = helper.generatePersonList(p3, p1, p4, p2); - AddressBook expectedAB = helper.generateAddressBook(fourPersons); - List expectedList = fourPersons; - helper.addToModel(model, fourPersons); - - assertCommandBehavior("find KEY", - Command.getMessageForPersonListShownSummary(expectedList.size()), - expectedAB, - expectedList); - } - - @Test - public void execute_find_matchesIfAnyKeywordPresent() throws Exception { - TestDataHelper helper = new TestDataHelper(); - Person pTarget1 = helper.generatePersonWithName("bla bla KEY bla"); - Person pTarget2 = helper.generatePersonWithName("bla rAnDoM bla bceofeia"); - Person pTarget3 = helper.generatePersonWithName("key key"); - Person p1 = helper.generatePersonWithName("sduauo"); - - List fourPersons = helper.generatePersonList(pTarget1, p1, pTarget2, pTarget3); - AddressBook expectedAB = helper.generateAddressBook(fourPersons); - List expectedList = helper.generatePersonList(pTarget1, pTarget2, pTarget3); - helper.addToModel(model, fourPersons); - - assertCommandBehavior("find key rAnDoM", - Command.getMessageForPersonListShownSummary(expectedList.size()), - expectedAB, - expectedList); - } - - - /** - * A utility class to generate test data. - */ - class TestDataHelper{ - - Person adam() throws Exception { - Name name = new Name("Adam Brown"); - Phone privatePhone = new Phone("111111"); - Email email = new Email("adam@gmail.com"); - Address privateAddress = new Address("111, alpha street"); - Tag tag1 = new Tag("tag1"); - Tag tag2 = new Tag("tag2"); - UniqueTagList tags = new UniqueTagList(tag1, tag2); - return new Person(name, privatePhone, email, privateAddress, tags); - } - - /** - * Generates a valid person using the given seed. - * Running this function with the same parameter values guarantees the returned person will have the same state. - * Each unique seed will generate a unique Person object. - * - * @param seed used to generate the person data field values - */ - Person generatePerson(int seed) throws Exception { - return new Person( - new Name("Person " + seed), - new Phone("" + Math.abs(seed)), - new Email(seed + "@email"), - new Address("House of " + seed), - new UniqueTagList(new Tag("tag" + Math.abs(seed)), new Tag("tag" + Math.abs(seed + 1))) - ); - } - - /** Generates the correct add command based on the person given */ - String generateAddCommand(Person p) { - StringBuffer cmd = new StringBuffer(); - - cmd.append("add "); - - cmd.append(p.getName().toString()); - cmd.append(" p/").append(p.getPhone()); - cmd.append(" e/").append(p.getEmail()); - cmd.append(" a/").append(p.getAddress()); - - UniqueTagList tags = p.getTags(); - for(Tag t: tags){ - cmd.append(" t/").append(t.tagName); - } - - return cmd.toString(); - } - - /** - * Generates an AddressBook with auto-generated persons. - */ - AddressBook generateAddressBook(int numGenerated) throws Exception{ - AddressBook addressBook = new AddressBook(); - addToAddressBook(addressBook, numGenerated); - return addressBook; - } - - /** - * Generates an AddressBook based on the list of Persons given. - */ - AddressBook generateAddressBook(List persons) throws Exception{ - AddressBook addressBook = new AddressBook(); - addToAddressBook(addressBook, persons); - return addressBook; - } - - /** - * Adds auto-generated Person objects to the given AddressBook - * @param addressBook The AddressBook to which the Persons will be added - */ - void addToAddressBook(AddressBook addressBook, int numGenerated) throws Exception{ - addToAddressBook(addressBook, generatePersonList(numGenerated)); - } - - /** - * Adds the given list of Persons to the given AddressBook - */ - void addToAddressBook(AddressBook addressBook, List personsToAdd) throws Exception{ - for(Person p: personsToAdd){ - addressBook.addPerson(p); - } - } - - /** - * Adds auto-generated Person objects to the given model - * @param model The model to which the Persons will be added - */ - void addToModel(Model model, int numGenerated) throws Exception{ - addToModel(model, generatePersonList(numGenerated)); - } - - /** - * Adds the given list of Persons to the given model - */ - void addToModel(Model model, List personsToAdd) throws Exception{ - for(Person p: personsToAdd){ - model.addPerson(p); - } - } - - /** - * Generates a list of Persons based on the flags. - */ - List generatePersonList(int numGenerated) throws Exception{ - List persons = new ArrayList<>(); - for(int i = 1; i <= numGenerated; i++){ - persons.add(generatePerson(i)); - } - return persons; - } - - List generatePersonList(Person... persons) { - return Arrays.asList(persons); - } - - /** - * Generates a Person object with given name. Other fields will have some dummy values. - */ - Person generatePersonWithName(String name) throws Exception { - return new Person( - new Name(name), - new Phone("1"), - new Email("1@email"), - new Address("House of 1"), - new UniqueTagList(new Tag("tag")) - ); - } - } -} diff --git a/src/test/java/seedu/address/model/UnmodifiableObservableListTest.java b/src/test/java/seedu/address/model/UnmodifiableObservableListTest.java deleted file mode 100644 index 0334d7e42073..000000000000 --- a/src/test/java/seedu/address/model/UnmodifiableObservableListTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package seedu.address.model; - -import javafx.collections.FXCollections; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import seedu.address.commons.core.UnmodifiableObservableList; - -import java.util.*; - -import static org.junit.Assert.assertSame; -import static seedu.address.testutil.TestUtil.assertThrows; - -public class UnmodifiableObservableListTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - List backing; - UnmodifiableObservableList list; - - @Before - public void setup() { - backing = new ArrayList<>(); - backing.add(10); - list = new UnmodifiableObservableList<>(FXCollections.observableList(backing)); - } - - @Test - public void transformationListGenerators_correctBackingList() { - assertSame(list.sorted().getSource(), list); - assertSame(list.filtered(i -> true).getSource(), list); - } - - @Test - public void mutatingMethods_disabled() { - - final Class ex = UnsupportedOperationException.class; - - assertThrows(ex, () -> list.add(0, 2)); - assertThrows(ex, () -> list.add(3)); - - assertThrows(ex, () -> list.addAll(2, 1)); - assertThrows(ex, () -> list.addAll(backing)); - assertThrows(ex, () -> list.addAll(0, backing)); - - assertThrows(ex, () -> list.set(0, 2)); - - assertThrows(ex, () -> list.setAll(new ArrayList())); - assertThrows(ex, () -> list.setAll(1, 2)); - - assertThrows(ex, () -> list.remove(0, 1)); - assertThrows(ex, () -> list.remove(null)); - assertThrows(ex, () -> list.remove(0)); - - assertThrows(ex, () -> list.removeAll(backing)); - assertThrows(ex, () -> list.removeAll(1, 2)); - - assertThrows(ex, () -> list.retainAll(backing)); - assertThrows(ex, () -> list.retainAll(1, 2)); - - assertThrows(ex, () -> list.replaceAll(i -> 1)); - - assertThrows(ex, () -> list.sort(Comparator.naturalOrder())); - - assertThrows(ex, () -> list.clear()); - - final Iterator iter = list.iterator(); - iter.next(); - assertThrows(ex, iter::remove); - - final ListIterator liter = list.listIterator(); - liter.next(); - assertThrows(ex, liter::remove); - assertThrows(ex, () -> liter.add(5)); - assertThrows(ex, () -> liter.set(3)); - assertThrows(ex, () -> list.removeIf(i -> true)); - } -} diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/seedu/address/storage/StorageManagerTest.java deleted file mode 100644 index 6780feab6afc..000000000000 --- a/src/test/java/seedu/address/storage/StorageManagerTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package seedu.address.storage; - - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.events.storage.DataSavingExceptionEvent; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.UserPrefs; -import seedu.address.testutil.TypicalTestPersons; -import seedu.address.testutil.EventsCollector; - -import java.io.IOException; - -import static junit.framework.TestCase.assertNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class StorageManagerTest { - - private StorageManager storageManager; - - @Rule - public TemporaryFolder testFolder = new TemporaryFolder(); - - - @Before - public void setup() { - storageManager = new StorageManager(getTempFilePath("ab"), getTempFilePath("prefs")); - } - - - private String getTempFilePath(String fileName) { - return testFolder.getRoot().getPath() + fileName; - } - - - /* - * Note: This is an integration test that verifies the StorageManager is properly wired to the - * {@link JsonUserPrefsStorage} class. - * More extensive testing of UserPref saving/reading is done in {@link JsonUserPrefsStorageTest} class. - */ - - @Test - public void prefsReadSave() throws Exception { - UserPrefs original = new UserPrefs(); - original.setGuiSettings(300, 600, 4, 6); - storageManager.saveUserPrefs(original); - UserPrefs retrieved = storageManager.readUserPrefs().get(); - assertEquals(original, retrieved); - } - - @Test - public void addressBookReadSave() throws Exception { - AddressBook original = new TypicalTestPersons().getTypicalAddressBook(); - storageManager.saveAddressBook(original); - ReadOnlyAddressBook retrieved = storageManager.readAddressBook().get(); - assertEquals(original, new AddressBook(retrieved)); - //More extensive testing of AddressBook saving/reading is done in XmlAddressBookStorageTest - } - - @Test - public void getAddressBookFilePath(){ - assertNotNull(storageManager.getAddressBookFilePath()); - } - - @Test - public void handleAddressBookChangedEvent_exceptionThrown_eventRaised() throws IOException { - //Create a StorageManager while injecting a stub that throws an exception when the save method is called - Storage storage = new StorageManager(new XmlAddressBookStorageExceptionThrowingStub("dummy"), new JsonUserPrefsStorage("dummy")); - EventsCollector eventCollector = new EventsCollector(); - storage.handleAddressBookChangedEvent(new AddressBookChangedEvent(new AddressBook())); - assertTrue(eventCollector.get(0) instanceof DataSavingExceptionEvent); - } - - - /** - * A Stub class to throw an exception when the save method is called - */ - class XmlAddressBookStorageExceptionThrowingStub extends XmlAddressBookStorage{ - - public XmlAddressBookStorageExceptionThrowingStub(String filePath) { - super(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) throws IOException { - throw new IOException("dummy exception"); - } - } - - -} diff --git a/src/test/java/seedu/address/storage/XmlAddressBookStorageTest.java b/src/test/java/seedu/address/storage/XmlAddressBookStorageTest.java deleted file mode 100644 index 04b0db1ce1c7..000000000000 --- a/src/test/java/seedu/address/storage/XmlAddressBookStorageTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package seedu.address.storage; - - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.TemporaryFolder; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.FileUtil; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.testutil.TypicalTestPersons; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -public class XmlAddressBookStorageTest { - private static String TEST_DATA_FOLDER = FileUtil.getPath("./src/test/data/XmlAddressBookStorageTest/"); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Rule - public TemporaryFolder testFolder = new TemporaryFolder(); - - @Test - public void readAddressBook_nullFilePath_assertionFailure() throws Exception { - thrown.expect(AssertionError.class); - readAddressBook(null); - } - - private java.util.Optional readAddressBook(String filePath) throws Exception { - return new XmlAddressBookStorage(filePath).readAddressBook(addToTestDataPathIfNotNull(filePath)); - } - - private String addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { - return prefsFileInTestDataFolder != null - ? TEST_DATA_FOLDER + prefsFileInTestDataFolder - : null; - } - - @Test - public void read_missingFile_emptyResult() throws Exception { - assertFalse(readAddressBook("NonExistentFile.xml").isPresent()); - } - - @Test - public void read_notXmlFormat_exceptionThrown() throws Exception { - - thrown.expect(DataConversionException.class); - readAddressBook("NotXmlFormatAddressBook.xml"); - - /* IMPORTANT: Any code below an exception-throwing line (like the one above) will be ignored. - * That means you should not have more than one exception test in one method - */ - } - - @Test - public void readAndSaveAddressBook_allInOrder_success() throws Exception { - String filePath = testFolder.getRoot().getPath() + "TempAddressBook.xml"; - TypicalTestPersons td = new TypicalTestPersons(); - AddressBook original = td.getTypicalAddressBook(); - XmlAddressBookStorage xmlAddressBookStorage = new XmlAddressBookStorage(filePath); - - //Save in new file and read back - xmlAddressBookStorage.saveAddressBook(original, filePath); - ReadOnlyAddressBook readBack = xmlAddressBookStorage.readAddressBook(filePath).get(); - assertEquals(original, new AddressBook(readBack)); - - //Modify data, overwrite exiting file, and read back - original.addPerson(new Person(TypicalTestPersons.hoon)); - original.removePerson(new Person(TypicalTestPersons.alice)); - xmlAddressBookStorage.saveAddressBook(original, filePath); - readBack = xmlAddressBookStorage.readAddressBook(filePath).get(); - assertEquals(original, new AddressBook(readBack)); - - //Save and read without specifying file path - original.addPerson(new Person(TypicalTestPersons.ida)); - xmlAddressBookStorage.saveAddressBook(original); //file path not specified - readBack = xmlAddressBookStorage.readAddressBook().get(); //file path not specified - assertEquals(original, new AddressBook(readBack)); - - } - - @Test - public void saveAddressBook_nullAddressBook_assertionFailure() throws IOException { - thrown.expect(AssertionError.class); - saveAddressBook(null, "SomeFile.xml"); - } - - private void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) throws IOException { - new XmlAddressBookStorage(filePath).saveAddressBook(addressBook, addToTestDataPathIfNotNull(filePath)); - } - - @Test - public void saveAddressBook_nullFilePath_assertionFailure() throws IOException { - thrown.expect(AssertionError.class); - saveAddressBook(new AddressBook(), null); - } - - -} diff --git a/src/test/java/seedu/address/testutil/AddressBookBuilder.java b/src/test/java/seedu/address/testutil/AddressBookBuilder.java deleted file mode 100644 index a623b81c878f..000000000000 --- a/src/test/java/seedu/address/testutil/AddressBookBuilder.java +++ /dev/null @@ -1,35 +0,0 @@ -package seedu.address.testutil; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; -import seedu.address.model.AddressBook; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * A utility class to help with building Addressbook objects. - * Example usage:
- * {@code AddressBook ab = new AddressBookBuilder().withPerson("John", "Doe").withTag("Friend").build();} - */ -public class AddressBookBuilder { - - private AddressBook addressBook; - - public AddressBookBuilder(AddressBook addressBook){ - this.addressBook = addressBook; - } - - public AddressBookBuilder withPerson(Person person) throws UniquePersonList.DuplicatePersonException { - addressBook.addPerson(person); - return this; - } - - public AddressBookBuilder withTag(String tagName) throws IllegalValueException { - addressBook.addTag(new Tag(tagName)); - return this; - } - - public AddressBook build(){ - return addressBook; - } -} diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java deleted file mode 100644 index 8b02a1668ef6..000000000000 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ /dev/null @@ -1,49 +0,0 @@ -package seedu.address.testutil; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; -import seedu.address.model.person.*; - -/** - * - */ -public class PersonBuilder { - - private TestPerson person; - - public PersonBuilder() { - this.person = new TestPerson(); - } - - public PersonBuilder withName(String name) throws IllegalValueException { - this.person.setName(new Name(name)); - return this; - } - - public PersonBuilder withTags(String ... tags) throws IllegalValueException { - for (String tag: tags) { - person.getTags().add(new Tag(tag)); - } - return this; - } - - public PersonBuilder withAddress(String address) throws IllegalValueException { - this.person.setAddress(new Address(address)); - return this; - } - - public PersonBuilder withPhone(String phone) throws IllegalValueException { - this.person.setPhone(new Phone(phone)); - return this; - } - - public PersonBuilder withEmail(String email) throws IllegalValueException { - this.person.setEmail(new Email(email)); - return this; - } - - public TestPerson build() { - return this.person; - } - -} diff --git a/src/test/java/seedu/address/testutil/TestPerson.java b/src/test/java/seedu/address/testutil/TestPerson.java deleted file mode 100644 index 19ee5ded1cd3..000000000000 --- a/src/test/java/seedu/address/testutil/TestPerson.java +++ /dev/null @@ -1,76 +0,0 @@ -package seedu.address.testutil; - -import seedu.address.model.tag.UniqueTagList; -import seedu.address.model.person.*; - -/** - * A mutable person object. For testing only. - */ -public class TestPerson implements ReadOnlyPerson { - - private Name name; - private Address address; - private Email email; - private Phone phone; - private UniqueTagList tags; - - public TestPerson() { - tags = new UniqueTagList(); - } - - public void setName(Name name) { - this.name = name; - } - - public void setAddress(Address address) { - this.address = address; - } - - public void setEmail(Email email) { - this.email = email; - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - @Override - public Name getName() { - return name; - } - - @Override - public Phone getPhone() { - return phone; - } - - @Override - public Email getEmail() { - return email; - } - - @Override - public Address getAddress() { - return address; - } - - @Override - public UniqueTagList getTags() { - return tags; - } - - @Override - public String toString() { - return getAsText(); - } - - public String getAddCommand() { - StringBuilder sb = new StringBuilder(); - sb.append("add " + this.getName().fullName + " "); - sb.append("p/" + this.getPhone().value + " "); - sb.append("e/" + this.getEmail().value + " "); - sb.append("a/" + this.getAddress().value + " "); - this.getTags().getInternalList().stream().forEach(s -> sb.append("t/" + s.tagName + " ")); - return sb.toString(); - } -} diff --git a/src/test/java/seedu/address/testutil/TypicalTestPersons.java b/src/test/java/seedu/address/testutil/TypicalTestPersons.java deleted file mode 100644 index 773f64a98cc3..000000000000 --- a/src/test/java/seedu/address/testutil/TypicalTestPersons.java +++ /dev/null @@ -1,61 +0,0 @@ -package seedu.address.testutil; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.person.*; - -/** - * - */ -public class TypicalTestPersons { - - public static TestPerson alice, benson, carl, daniel, elle, fiona, george, hoon, ida; - - public TypicalTestPersons() { - try { - alice = new PersonBuilder().withName("Alice Pauline").withAddress("123, Jurong West Ave 6, #08-111") - .withEmail("alice@gmail.com").withPhone("85355255") - .withTags("friends").build(); - benson = new PersonBuilder().withName("Benson Meier").withAddress("311, Clementi Ave 2, #02-25") - .withEmail("johnd@gmail.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); - carl = new PersonBuilder().withName("Carl Kurz").withPhone("95352563").withEmail("heinz@yahoo.com").withAddress("wall street").build(); - daniel = new PersonBuilder().withName("Daniel Meier").withPhone("87652533").withEmail("cornelia@google.com").withAddress("10th street").build(); - elle = new PersonBuilder().withName("Elle Meyer").withPhone("9482224").withEmail("werner@gmail.com").withAddress("michegan ave").build(); - fiona = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427").withEmail("lydia@gmail.com").withAddress("little tokyo").build(); - george = new PersonBuilder().withName("George Best").withPhone("9482442").withEmail("anna@google.com").withAddress("4th street").build(); - - //Manually added - hoon = new PersonBuilder().withName("Hoon Meier").withPhone("8482424").withEmail("stefan@mail.com").withAddress("little india").build(); - ida = new PersonBuilder().withName("Ida Mueller").withPhone("8482131").withEmail("hans@google.com").withAddress("chicago ave").build(); - } catch (IllegalValueException e) { - e.printStackTrace(); - assert false : "not possible"; - } - } - - public static void loadAddressBookWithSampleData(AddressBook ab) { - - try { - ab.addPerson(new Person(alice)); - ab.addPerson(new Person(benson)); - ab.addPerson(new Person(carl)); - ab.addPerson(new Person(daniel)); - ab.addPerson(new Person(elle)); - ab.addPerson(new Person(fiona)); - ab.addPerson(new Person(george)); - } catch (UniquePersonList.DuplicatePersonException e) { - assert false : "not possible"; - } - } - - public TestPerson[] getTypicalPersons() { - return new TestPerson[]{alice, benson, carl, daniel, elle, fiona, george}; - } - - public AddressBook getTypicalAddressBook(){ - AddressBook ab = new AddressBook(); - loadAddressBookWithSampleData(ab); - return ab; - } -} diff --git a/src/test/java/seedu/agendum/MainAppTest.java b/src/test/java/seedu/agendum/MainAppTest.java new file mode 100644 index 000000000000..983755b181b5 --- /dev/null +++ b/src/test/java/seedu/agendum/MainAppTest.java @@ -0,0 +1,269 @@ +package seedu.agendum; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.util.Hashtable; +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.ConfigUtil; +import seedu.agendum.logic.commands.CommandLibrary; +import seedu.agendum.model.UserPrefs; +import seedu.agendum.storage.JsonAliasTableStorage; +import seedu.agendum.storage.JsonUserPrefsStorage; +import seedu.agendum.storage.StorageManager; +import seedu.agendum.testutil.TestUtil; + +//@@author A0148095X +public class MainAppTest { + + private MainApp mainApp; + + private Config defaultConfig; + private UserPrefs defaultUserPrefs; + private Hashtable defaultAliasTable; + + // user prefs and alias table filepaths lead to empty files + private Config configWithBadFilePaths; + // user prefs and alias table filepaths lead to read only files + private Config configWithReadOnlyFilePaths; + + private final String pathToBadConfig = TestUtil.getFilePathInSandboxFolder("bad_config.json"); + private final String pathToReadOnlyConfig = TestUtil.getFilePathInSandboxFolder("read_only_config.json"); + + private final String pathToBadUserPrefs = TestUtil.getFilePathInSandboxFolder("bad_user_prefs.json"); + private final String pathToReadOnlyUserPrefs = TestUtil.getFilePathInSandboxFolder("read_only_user_prefs.json"); + + private final String pathToBadAliasTable = TestUtil.getFilePathInSandboxFolder("bad_alias_table.json"); + private final String pathToReadOnlyAliasTable = TestUtil.getFilePathInSandboxFolder("read_only_alias_table.json"); + + @Before + public void setUp() { + mainApp = new MainApp(); + + defaultConfig = new Config(); + defaultUserPrefs = new UserPrefs(); + defaultAliasTable = new Hashtable(); + + configWithBadFilePaths = generateConfigWithBadFilePaths(); + configWithReadOnlyFilePaths = generateConfigWithReadOnlyFilePaths(); + + createEmptyFile(pathToBadConfig); + createReadOnlyConfigFile(pathToReadOnlyConfig); + + createEmptyFile(pathToBadUserPrefs); + createReadOnlyUserPrefsFile(pathToReadOnlyUserPrefs); + + createEmptyFile(pathToBadAliasTable); + createReadOnlyAliasTableFile(pathToReadOnlyAliasTable); + } + + @Test + public void initConfig_nullFilePath_returnsDefaultConfig() { + Config config = mainApp.initConfig(null); + assertEquals(config, defaultConfig); + } + + @Test + public void initConfig_validFilePathInvalidFileFormat_returnsDefaultConfig() { + Config config = mainApp.initConfig(pathToBadConfig); + assertEquals(config, defaultConfig); + } + + @Test + public void initConfig_validFilePathValidFormatReadOnly_returnsDefaultConfigLogsWarning() { + Config config = mainApp.initConfig(pathToReadOnlyConfig); + assertEquals(config, defaultConfig); + } + + @Test + public void initPrefs_invalidFileFormat_returnsDefaultUserPrefs() { + // Set up storage to point to bad user prefs + mainApp.storage = new StorageManager("", "", pathToBadUserPrefs, null); + + UserPrefs userPrefs = mainApp.initPrefs(configWithBadFilePaths); + assertEquals(userPrefs, defaultUserPrefs); + + // reset storage + mainApp.storage = null; + } + + @Test + public void initPrefs_validFileReadOnly_returnsDefaultUserPrefsLogsWarning() { + // Set up storage to point to read only prefs + mainApp.storage = new StorageManager("", "", pathToReadOnlyUserPrefs, null); + + UserPrefs userPrefs = mainApp.initPrefs(configWithReadOnlyFilePaths); + assertEquals(userPrefs, defaultUserPrefs); + + // reset storage + mainApp.storage = null; + } + + @Test + public void initPrefs_exceptionThrowingStorage_returnsDefaultUserPrefsLogsWarning() { + mainApp.storage = new ReadUserPrefsExceptionThrowingStorageManagerStub(); + + UserPrefs userPrefs = mainApp.initPrefs(defaultConfig); + assertEquals(userPrefs, defaultUserPrefs); + + // reset storage + mainApp.storage = null; + } + + @Test + public void initAliasTable_invalidFileFormat_returnsEmptyHashtable() { + // Set up storage to point to bad alias table + mainApp.storage = new StorageManager("", pathToBadAliasTable, "", null); + + mainApp.initAliasTable(configWithBadFilePaths); + Hashtable actualAliasTable = CommandLibrary.getInstance().getAliasTable(); + assertEquals(actualAliasTable, defaultAliasTable); + + // reset storage and alias table + mainApp.storage = null; + CommandLibrary.getInstance().loadAliasTable(null); + } + + @Test + public void initAliasTable_validFileFormatReadOnly_returnsEmptyHashtable() { + // Set up storage to point to read only alias table + mainApp.storage = new StorageManager("", pathToReadOnlyAliasTable, "", null); + + mainApp.initAliasTable(configWithReadOnlyFilePaths); + Hashtable actualAliasTable = CommandLibrary.getInstance().getAliasTable(); + assertEquals(actualAliasTable, defaultAliasTable); + + // reset storage and alias table + mainApp.storage = null; + CommandLibrary.getInstance().loadAliasTable(null); + } + + @Test + public void initAliasTable_exceptionThrowingStorage_returnsDefaultHashtableLogsWarning() { + mainApp.storage = new ReadAliasTableExceptionThrowingStorageManagerStub(); + + mainApp.initAliasTable(defaultConfig); + Hashtable actualAliasTable = CommandLibrary.getInstance().getAliasTable(); + assertEquals(actualAliasTable, defaultAliasTable); + + // reset storage and alias table + mainApp.storage = null; + CommandLibrary.getInstance().loadAliasTable(null); + } + + private void createEmptyFile(String filePath) { + File file = new File(filePath); + + deleteIfExists(file); + + try { + file.createNewFile(); + } catch (IOException e) { + Assert.fail("Error creating empty file at: " + filePath); + } + } + + private void createReadOnlyConfigFile(String filePath) { + File file = new File(filePath); + + // Ensure that the file is empty + deleteIfExists(file); + + try { + ConfigUtil.saveConfig(defaultConfig, filePath); + } catch (IOException e) { + Assert.fail("Error creating read only config file"); + } + + if (!file.setReadOnly()) { + Assert.fail("Unable to set read only config to read only"); + } + } + + private void createReadOnlyUserPrefsFile(String filePath) { + File file = new File(filePath); + + // Ensure that the file is empty + deleteIfExists(file); + + try { + JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(filePath); + userPrefsStorage.saveUserPrefs(defaultUserPrefs, filePath); + } catch (IOException e) { + Assert.fail("Error creating read only user prefs file"); + } + + if (!file.setReadOnly()) { + Assert.fail("Unable to set read only user prefs to read only"); + } + } + + private void createReadOnlyAliasTableFile(String filePath) { + File file = new File(filePath); + + // Ensure that the file is empty + deleteIfExists(file); + + try { + JsonAliasTableStorage aliasTableStorage = new JsonAliasTableStorage(filePath); + aliasTableStorage.saveAliasTable(defaultAliasTable, filePath); + } catch (IOException e) { + Assert.fail("Error creating read only alias table file"); + } + + if (!file.setReadOnly()) { + Assert.fail("Unable to set read only alias table to read only"); + } + } + + public void deleteIfExists(File file) { + if (file.exists()) { + file.delete(); + } + } + + private Config generateConfigWithReadOnlyFilePaths() { + Config config = new Config(); + config.setAliasTableFilePath(pathToReadOnlyAliasTable); + config.setUserPrefsFilePath(pathToReadOnlyUserPrefs); + return config; + } + + private Config generateConfigWithBadFilePaths() { + Config config = new Config(); + config.setAliasTableFilePath(pathToBadAliasTable); + config.setUserPrefsFilePath(pathToBadUserPrefs); + return config; + } + + /** Throws an IOException when readUserPrefs is called **/ + class ReadUserPrefsExceptionThrowingStorageManagerStub extends StorageManager { + public ReadUserPrefsExceptionThrowingStorageManagerStub() { + super("", "", "", null); + } + + public Optional readUserPrefs() throws IOException { + throw new IOException(this.getClass().getCanonicalName() +": IOException"); + } + } + + /** Throws an IOException when readAliasTable is called **/ + class ReadAliasTableExceptionThrowingStorageManagerStub extends StorageManager { + public ReadAliasTableExceptionThrowingStorageManagerStub() { + super("", "", "", null); + } + + @Override + public Optional> readAliasTable() throws DataConversionException, IOException { + throw new IOException(this.getClass().getCanonicalName() + ": IOException"); + } + } +} diff --git a/src/test/java/seedu/address/TestApp.java b/src/test/java/seedu/agendum/TestApp.java similarity index 61% rename from src/test/java/seedu/address/TestApp.java rename to src/test/java/seedu/agendum/TestApp.java index 756642b6c180..9e4ddd1dfe42 100644 --- a/src/test/java/seedu/address/TestApp.java +++ b/src/test/java/seedu/agendum/TestApp.java @@ -1,13 +1,13 @@ -package seedu.address; +package seedu.agendum; import javafx.stage.Screen; import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.UserPrefs; -import seedu.address.storage.XmlSerializableAddressBook; -import seedu.address.testutil.TestUtil; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.core.GuiSettings; +import seedu.agendum.model.ReadOnlyToDoList; +import seedu.agendum.model.UserPrefs; +import seedu.agendum.storage.XmlSerializableToDoList; +import seedu.agendum.testutil.TestUtil; import java.util.function.Supplier; @@ -18,16 +18,15 @@ public class TestApp extends MainApp { public static final String SAVE_LOCATION_FOR_TESTING = TestUtil.getFilePathInSandboxFolder("sampleData.xml"); - protected static final String DEFAULT_PREF_FILE_LOCATION_FOR_TESTING = TestUtil.getFilePathInSandboxFolder("pref_testing.json"); public static final String APP_TITLE = "Test App"; - protected static final String ADDRESS_BOOK_NAME = "Test"; - protected Supplier initialDataSupplier = () -> null; + protected static final String TO_DO_LIST_NAME = "Test"; + protected Supplier initialDataSupplier = () -> null; protected String saveFileLocation = SAVE_LOCATION_FOR_TESTING; public TestApp() { } - public TestApp(Supplier initialDataSupplier, String saveFileLocation) { + public TestApp(Supplier initialDataSupplier, String saveFileLocation) { super(); this.initialDataSupplier = initialDataSupplier; this.saveFileLocation = saveFileLocation; @@ -35,18 +34,17 @@ public TestApp(Supplier initialDataSupplier, String saveFil // If some initial local data has been provided, write those to the file if (initialDataSupplier.get() != null) { TestUtil.createDataFileWithData( - new XmlSerializableAddressBook(this.initialDataSupplier.get()), + new XmlSerializableToDoList(this.initialDataSupplier.get()), this.saveFileLocation); } } @Override protected Config initConfig(String configFilePath) { - Config config = super.initConfig(configFilePath); + Config config = TestUtil.createTempConfig(); config.setAppTitle(APP_TITLE); - config.setAddressBookFilePath(saveFileLocation); - config.setUserPrefsFilePath(DEFAULT_PREF_FILE_LOCATION_FOR_TESTING); - config.setAddressBookName(ADDRESS_BOOK_NAME); + config.setToDoListFilePath(saveFileLocation); + config.setToDoListName(TO_DO_LIST_NAME); return config; } diff --git a/src/test/java/seedu/agendum/commons/core/ConfigTest.java b/src/test/java/seedu/agendum/commons/core/ConfigTest.java new file mode 100644 index 000000000000..86fee3e1721a --- /dev/null +++ b/src/test/java/seedu/agendum/commons/core/ConfigTest.java @@ -0,0 +1,63 @@ +package seedu.agendum.commons.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +public class ConfigTest { + + private Config one; + private Config another; + + @Before + public void setUp() { + one = new Config(); + another = new Config(); + } + + @Test + public void toString_defaultObject_stringReturned() { + StringBuilder sb = new StringBuilder(); + sb.append("App title : Agendum"); + sb.append("\nCurrent log level : INFO"); + sb.append("\nAlias Table file location: " + Config.DEFAULT_ALIAS_TABLE_FILE); + sb.append("\nPreference file Location : " + Config.DEFAULT_USER_PREFS_FILE); + sb.append("\nLocal data file location : " + Config.DEFAULT_SAVE_LOCATION); + sb.append("\nToDoList name : MyToDoList"); + + assertEquals(sb.toString(), new Config().toString()); + } + + //@@author A0148095X + @Test + public void setAliasTableFilePath_validPath_returnsTrue() { + Config config = new Config(); + String validPath = "dropbox/table.xml"; + config.setAliasTableFilePath(validPath); + + assertEquals(validPath, config.getAliasTableFilePath()); + } + + @Test + public void equals_differentObjectType_returnsFalse() { + assertFalse(one.equals(new Object())); + } + + @Test + public void equals_same_returnsTrue() { + assertTrue(one.equals(one)); + } + + @Test + public void equals_symmetric_returnsTrue() { + assertTrue(one.equals(another) && another.equals(one)); + } + + @Test + public void hashCode_symmetric_returnsTrue() { + assertTrue(one.hashCode() == another.hashCode()); + } +} diff --git a/src/test/java/seedu/agendum/commons/core/GuiSettingsTest.java b/src/test/java/seedu/agendum/commons/core/GuiSettingsTest.java new file mode 100644 index 000000000000..8853c31675a5 --- /dev/null +++ b/src/test/java/seedu/agendum/commons/core/GuiSettingsTest.java @@ -0,0 +1,69 @@ +package seedu.agendum.commons.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +//@@author A0148095X +public class GuiSettingsTest { + + private Double windowWidth = 800.0; + private Double windowHeight = 600.0; + private int xPosition = 100; + private int yPosition = 300; + + private GuiSettings one, another; + + @Before + public void setUp() { + one = new GuiSettings(windowWidth, windowHeight, xPosition, yPosition); + another = new GuiSettings(windowWidth, windowHeight, xPosition, yPosition); + } + + @Test + public void equals_differentObject_returnsFalse() { + assertFalse(one.equals(new Object())); + } + + @Test + public void equals_symmetric_returnsTrue() { + // equals to itself and similar object + assertTrue(one.equals(one)); + assertTrue(one.equals(another)); + } + + @Test + public void equals_validInputDifferentSettings_returnsFalse() { + // ----------- different settings ---------------- + GuiSettings differentSettings; + + Double differentWindowWidth = windowWidth*2; + Double differentWindowHeight = windowHeight*2; + int differentXPosition = xPosition*2; + int differentYPosition = yPosition*2; + + // different width + differentSettings = new GuiSettings(differentWindowWidth, windowHeight, xPosition, yPosition); + assertFalse(one.equals(differentSettings)); + + // different height + differentSettings = new GuiSettings(windowWidth, differentWindowHeight, xPosition, yPosition); + assertFalse(one.equals(differentSettings)); + + // different x position + differentSettings = new GuiSettings(windowWidth, windowHeight, differentXPosition, yPosition); + assertFalse(one.equals(differentSettings)); + + // different y position + differentSettings = new GuiSettings(windowWidth, windowHeight, xPosition, differentYPosition); + assertFalse(one.equals(differentSettings)); + } + + @Test + public void hashcode_symmetric_returnsTrue() { + assertEquals(one.hashCode(), another.hashCode()); + } +} diff --git a/src/test/java/seedu/address/commons/core/VersionTest.java b/src/test/java/seedu/agendum/commons/core/VersionTest.java similarity index 80% rename from src/test/java/seedu/address/commons/core/VersionTest.java rename to src/test/java/seedu/agendum/commons/core/VersionTest.java index 87ac01f6c92d..693bae392ea6 100644 --- a/src/test/java/seedu/address/commons/core/VersionTest.java +++ b/src/test/java/seedu/agendum/commons/core/VersionTest.java @@ -1,31 +1,27 @@ -package seedu.address.commons.core; +package seedu.agendum.commons.core; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class VersionTest { - @Rule - public ExpectedException thrown = ExpectedException.none(); @Test - public void versionParsing_acceptableVersionString_parsedVersionCorrectly() { + public void versionParsingAcceptableVersionStringParsedVersionCorrectly() { verifyVersionParsedCorrectly("V0.0.0ea", 0, 0, 0, true); verifyVersionParsedCorrectly("V3.10.2", 3, 10, 2, false); verifyVersionParsedCorrectly("V100.100.100ea", 100, 100, 100, true); } - @Test - public void versionParsing_wrongVersionString_throwIllegalArgumentException() { - thrown.expect(IllegalArgumentException.class); + @Test(expected = IllegalArgumentException.class) + public void versionParsingWrongVersionStringThrowIllegalArgumentException() { Version.fromString("This is not a version string"); } @Test - public void versionConstructor_correctParameter_valueAsExpected() { + public void versionConstructorCorrectParameterValueAsExpected() { Version version = new Version(19, 10, 20, true); assertEquals(19, version.getMajor()); @@ -35,7 +31,7 @@ public void versionConstructor_correctParameter_valueAsExpected() { } @Test - public void versionToString_validVersion_correctStringRepresentation() { + public void versionToStringValidVersionCorrectStringRepresentation() { // boundary at 0 Version version = new Version(0, 0, 0, true); assertEquals("V0.0.0ea", version.toString()); @@ -50,7 +46,7 @@ public void versionToString_validVersion_correctStringRepresentation() { } @Test - public void versionComparable_validVersion_compareToIsCorrect() { + public void versionComparableValidVersionCompareToIsCorrect() { Version one, another; // Tests equality @@ -109,7 +105,7 @@ public void versionComparable_validVersion_compareToIsCorrect() { } @Test - public void versionComparable_validVersion_hashCodeIsCorrect() { + public void versionComparableValidVersionHashCodeIsCorrect() { Version version = new Version(100, 100, 100, true); assertEquals(100100100, version.hashCode()); @@ -118,7 +114,7 @@ public void versionComparable_validVersion_hashCodeIsCorrect() { } @Test - public void versionComparable_validVersion_equalIsCorrect() { + public void versionComparableValidVersionEqualIsCorrect() { Version one, another; one = new Version(0, 0, 0, false); @@ -129,7 +125,22 @@ public void versionComparable_validVersion_equalIsCorrect() { another = new Version(100, 191, 275, true); assertTrue(one.equals(another)); } - + + //@@author A0148095X + @Test + public void versionComparableNotEqual() { + Version original = new Version(0, 0, 0, false); + + // null + Object nullObj = null; + assertFalse(original.equals(nullObj)); + + // Different object + Object obj = new Object(); + assertFalse(original.equals(obj)); + } + //@@author + private void verifyVersionParsedCorrectly(String versionString, int major, int minor, int patch, boolean isEarlyAccess) { assertEquals(new Version(major, minor, patch, isEarlyAccess), Version.fromString(versionString)); diff --git a/src/test/java/seedu/agendum/commons/util/AppUtilTest.java b/src/test/java/seedu/agendum/commons/util/AppUtilTest.java new file mode 100644 index 000000000000..ac44579ab79d --- /dev/null +++ b/src/test/java/seedu/agendum/commons/util/AppUtilTest.java @@ -0,0 +1,21 @@ +package seedu.agendum.commons.util; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +public class AppUtilTest { + + @Test + public void getImageExitingImage(){ + assertNotNull(AppUtil.getImage("/images/agendum_icon.png")); + } + + + @Test(expected = AssertionError.class) + public void getImageNullGivenAssertionError(){ + AppUtil.getImage(null); + } + + +} diff --git a/src/test/java/seedu/agendum/commons/util/CollectionUtilTest.java b/src/test/java/seedu/agendum/commons/util/CollectionUtilTest.java new file mode 100644 index 000000000000..d0a18bca7105 --- /dev/null +++ b/src/test/java/seedu/agendum/commons/util/CollectionUtilTest.java @@ -0,0 +1,63 @@ +package seedu.agendum.commons.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; + +import org.junit.Before; +import org.junit.Test; + +//@@author A0148095X +public class CollectionUtilTest { + + public String string = "string"; + public int number = 1; + public double decimal = 2.0; + public boolean bool = true; + public Object obj = new Object(); + + public ArrayList noNullUniqueArrayList; + + @Before + public void setUp() { + noNullUniqueArrayList = new ArrayList(); + noNullUniqueArrayList.add(string); + noNullUniqueArrayList.add(number); + noNullUniqueArrayList.add(decimal); + noNullUniqueArrayList.add(bool); + noNullUniqueArrayList.add(obj); + } + + @Test + public void isNotNull() { + // No nulls + assertTrue(CollectionUtil.isNotNull(string, number, decimal, bool, obj)); + + // One null + assertFalse(CollectionUtil.isNotNull(string, number, null, decimal, bool, obj)); + } + + @Test + public void assertNoNullNoNull() { + // No assertion errors; all non-null + CollectionUtil.assertNoNullElements(noNullUniqueArrayList); + } + + @Test(expected = AssertionError.class) + public void assertNoNullElementsNullCollection(){ + // The collection is null + CollectionUtil.assertNoNullElements(null); + } + + @Test + public void elementsAreUnique() { + // Unique + assertTrue(CollectionUtil.elementsAreUnique(noNullUniqueArrayList)); + + // Not unique + ArrayList notUniqueArrayList = new ArrayList<>(noNullUniqueArrayList); + notUniqueArrayList.add(string); + assertFalse(CollectionUtil.elementsAreUnique(notUniqueArrayList)); + } +} diff --git a/src/test/java/seedu/address/commons/util/ConfigUtilTest.java b/src/test/java/seedu/agendum/commons/util/ConfigUtilTest.java similarity index 61% rename from src/test/java/seedu/address/commons/util/ConfigUtilTest.java rename to src/test/java/seedu/agendum/commons/util/ConfigUtilTest.java index 6699343c4a82..d61451200e90 100644 --- a/src/test/java/seedu/address/commons/util/ConfigUtilTest.java +++ b/src/test/java/seedu/agendum/commons/util/ConfigUtilTest.java @@ -1,12 +1,11 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataConversionException; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.exceptions.DataConversionException; import java.io.File; import java.io.IOException; @@ -20,27 +19,22 @@ public class ConfigUtilTest { private static String TEST_DATA_FOLDER = FileUtil.getPath("./src/test/data/ConfigUtilTest/"); - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Rule public TemporaryFolder testFolder = new TemporaryFolder(); - @Test - public void read_null_assertionFailure() throws DataConversionException { - thrown.expect(AssertionError.class); + @Test(expected = AssertionError.class) + public void readNullAssertionFailure() throws DataConversionException { read(null); } @Test - public void read_missingFile_emptyResult() throws DataConversionException { + public void readMissingFileEmptyResult() throws DataConversionException { assertFalse(read("NonExistentFile.json").isPresent()); } - @Test - public void read_notJasonFormat_exceptionThrown() throws DataConversionException { + @Test(expected = DataConversionException.class) + public void readNotJasonFormatExceptionThrown() throws DataConversionException { - thrown.expect(DataConversionException.class); read("NotJasonFormatConfig.json"); /* IMPORTANT: Any code below an exception-throwing line (like the one above) will be ignored. @@ -49,7 +43,7 @@ public void read_notJasonFormat_exceptionThrown() throws DataConversionException } @Test - public void read_fileInOrder_successfullyRead() throws DataConversionException { + public void readFileInOrderSuccessfullyRead() throws DataConversionException { Config expected = getTypicalConfig(); @@ -58,13 +52,13 @@ public void read_fileInOrder_successfullyRead() throws DataConversionException { } @Test - public void read_valuesMissingFromFile_defaultValuesUsed() throws DataConversionException { + public void readValuesMissingFromFileDefaultValuesUsed() throws DataConversionException { Config actual = read("EmptyConfig.json").get(); assertEquals(new Config(), actual); } @Test - public void read_extraValuesInFile_extraValuesIgnored() throws DataConversionException { + public void readExtraValuesInFileExtraValuesIgnored() throws DataConversionException { Config expected = getTypicalConfig(); Config actual = read("ExtraValuesConfig.json").get(); @@ -76,51 +70,48 @@ private Config getTypicalConfig() { config.setAppTitle("Typical App Title"); config.setLogLevel(Level.INFO); config.setUserPrefsFilePath("C:\\preferences.json"); - config.setAddressBookFilePath("addressbook.xml"); - config.setAddressBookName("TypicalAddressBookName"); + config.setToDoListFilePath("todolist.xml"); + config.setToDoListName("TypicalToDoListName"); return config; } private Optional read(String configFileInTestDataFolder) throws DataConversionException { String configFilePath = addToTestDataPathIfNotNull(configFileInTestDataFolder); - return new ConfigUtil().readConfig(configFilePath); + return ConfigUtil.readConfig(configFilePath); } - @Test - public void save_nullConfig_assertionFailure() throws IOException { - thrown.expect(AssertionError.class); + @Test(expected = AssertionError.class) + public void saveNullConfigAssertionFailure() throws IOException { save(null, "SomeFile.json"); } - @Test - public void save_nullFile_assertionFailure() throws IOException { - thrown.expect(AssertionError.class); + @Test(expected = AssertionError.class) + public void saveNullFileAssertionFailure() throws IOException { save(new Config(), null); } @Test - public void saveConfig_allInOrder_success() throws DataConversionException, IOException { + public void saveConfigAllInOrderSuccess() throws DataConversionException, IOException { Config original = getTypicalConfig(); String configFilePath = testFolder.getRoot() + File.separator + "TempConfig.json"; - ConfigUtil configStorage = new ConfigUtil(); //Try writing when the file doesn't exist - configStorage.saveConfig(original, configFilePath); - Config readBack = configStorage.readConfig(configFilePath).get(); + ConfigUtil.saveConfig(original, configFilePath); + Config readBack = ConfigUtil.readConfig(configFilePath).get(); assertEquals(original, readBack); //Try saving when the file exists original.setAppTitle("Updated Title"); original.setLogLevel(Level.FINE); - configStorage.saveConfig(original, configFilePath); - readBack = configStorage.readConfig(configFilePath).get(); + ConfigUtil.saveConfig(original, configFilePath); + readBack = ConfigUtil.readConfig(configFilePath).get(); assertEquals(original, readBack); } private void save(Config config, String configFileInTestDataFolder) throws IOException { String configFilePath = addToTestDataPathIfNotNull(configFileInTestDataFolder); - new ConfigUtil().saveConfig(config, configFilePath); + ConfigUtil.saveConfig(config, configFilePath); } private String addToTestDataPathIfNotNull(String configFileInTestDataFolder) { diff --git a/src/test/java/seedu/agendum/commons/util/FileUtilTest.java b/src/test/java/seedu/agendum/commons/util/FileUtilTest.java new file mode 100644 index 000000000000..dd85ed28e002 --- /dev/null +++ b/src/test/java/seedu/agendum/commons/util/FileUtilTest.java @@ -0,0 +1,177 @@ +package seedu.agendum.commons.util; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.commons.exceptions.FileDeletionException; +import seedu.agendum.testutil.SerializableTestClass; +import seedu.agendum.testutil.TestUtil; + +public class FileUtilTest { + private static final File SERIALIZATION_FILE = new File(TestUtil.getFilePathInSandboxFolder("serialize.json")); + + private String filePathThatExists; + private File fileThatExists; + + private String filePathThatDoesNotExist; + private File fileThatDoesNotExist; + + private String filePathToBeDeleted; + private File fileToBeDeleted; + + private String filePathWithParentDirectories; + private File fileWithParentDirectories; + + private String filePathWithInvalidDirectoryName; + private File fileWithInvalidDirectoryName; + + private String filePathWithValidDirectoryName; + private File fileWithValidDirectoryName; + + //@@author A0148095X + @Before + public void setUp() throws IOException { + filePathThatExists = "file_that_exists.test"; + fileThatExists = new File(filePathThatExists); + FileUtil.createFile(fileThatExists); + + filePathThatDoesNotExist = "file_that_does_not_exist.test"; + fileThatDoesNotExist = new File(filePathThatDoesNotExist); + + filePathToBeDeleted = "file_to_be_deleted.test"; + fileToBeDeleted = new File(filePathToBeDeleted); + FileUtil.createFile(fileToBeDeleted); + + filePathWithParentDirectories = "data/test/file_with_parent_directories.test"; + fileWithParentDirectories = new File(filePathWithParentDirectories); + + filePathWithInvalidDirectoryName = "invalid \0 /0 directory"; + fileWithInvalidDirectoryName = new File(filePathWithInvalidDirectoryName); + + filePathWithValidDirectoryName = "validdirectory"; + fileWithValidDirectoryName = new File(filePathWithValidDirectoryName); + } + + @After + public void cleanup() throws IOException { + fileThatExists.delete(); + fileThatDoesNotExist.delete(); + fileToBeDeleted.delete(); + fileWithParentDirectories.delete(); + fileWithInvalidDirectoryName.delete(); + fileWithValidDirectoryName.delete(); + } + + @Test + public void isFileExists_validPathFileDoesNotExist_returnsFalse() { + assertFalse(FileUtil.isFileExists(filePathThatDoesNotExist)); + assertFalse(FileUtil.isFileExists(fileThatDoesNotExist)); + } + + @Test + public void isFileExists_fileExists_returnsTrue() { + assertTrue(FileUtil.isFileExists(filePathThatExists)); + assertTrue(FileUtil.isFileExists(fileThatExists)); + } + + @Test + public void createFile_validFilePathWithParentDirectories() throws IOException, FileDeletionException { + assertTrue(FileUtil.createFile(fileWithParentDirectories)); + // create file again to test when it already exists + assertFalse(FileUtil.createFile(fileWithParentDirectories)); + FileUtil.deleteFile(filePathWithParentDirectories); + } + + @Test(expected = AssertionError.class) + public void deleteFile_nullFilePath_throwsAssertionError() throws FileDeletionException { + // invalid filepath + FileUtil.deleteFile(null); + } + + @Test + public void deleteFile_validPathAndfile_success() throws FileDeletionException { + FileUtil.deleteFile(filePathToBeDeleted); + assertFalse(FileUtil.isFileExists(filePathToBeDeleted)); + } + + @Test (expected = FileDeletionException.class) + public void deleteFile_validPathInvalidFile_throwsFileDeletionException() throws FileDeletionException { + FileUtil.deleteFile(filePathThatDoesNotExist); + } + + @Test + public void isPathAvailable_validPathExistingFile_returnsTrue() { + assertTrue(FileUtil.isPathAvailable(filePathThatExists)); + } + + @Test + public void isPathAvailable_validPathNonExistingFile_returnsTrue() { + assertTrue(FileUtil.isPathAvailable(filePathThatDoesNotExist)); + } + + @Test + public void isPathAvailable_invalidPath_returnsFalse() { + assertFalse(FileUtil.isPathAvailable(filePathWithInvalidDirectoryName)); + } + + + @Test (expected = IOException.class) + public void createDirs_invalidDirectoryName_throwsIOException() throws IOException { + FileUtil.createDirs(fileWithInvalidDirectoryName); + } + + @Test + public void createDirs_validDirectoryName_success() throws IOException { + FileUtil.createDirs(fileWithValidDirectoryName); + } + //@@author + + @Test(expected = AssertionError.class) + public void getPathNullParameter(){ + // valid case + assertEquals("folder" + File.separator + "sub-folder", FileUtil.getPath("folder/sub-folder")); + + // null parameter -> assertion failure + FileUtil.getPath(null); + } + + @Test(expected = AssertionError.class) + public void getPathNoForwardSlash(){ + // valid case + assertEquals("folder" + File.separator + "sub-folder", FileUtil.getPath("folder/sub-folder")); + + // no forwards slash -> assertion failure + FileUtil.getPath("folder"); + } + + @Test + public void serializeObjectToJsonFile_noExceptionThrown() throws IOException { + SerializableTestClass serializableTestClass = new SerializableTestClass(); + serializableTestClass.setTestValues(); + + FileUtil.serializeObjectToJsonFile(SERIALIZATION_FILE, serializableTestClass); + + assertEquals(FileUtil.readFromFile(SERIALIZATION_FILE), SerializableTestClass.JSON_STRING_REPRESENTATION); + } + + @Test + public void deserializeObjectFromJsonFile_noExceptionThrown() throws IOException { + FileUtil.writeToFile(SERIALIZATION_FILE, SerializableTestClass.JSON_STRING_REPRESENTATION); + + SerializableTestClass serializableTestClass = FileUtil + .deserializeObjectFromJsonFile(SERIALIZATION_FILE, SerializableTestClass.class); + + assertEquals(serializableTestClass.getName(), SerializableTestClass.getNameTestValue()); + assertEquals(serializableTestClass.getListOfLocalDateTimes(), SerializableTestClass.getListTestValues()); + assertEquals(serializableTestClass.getMapOfIntegerToString(), SerializableTestClass.getHashMapTestValues()); + } +} diff --git a/src/test/java/seedu/address/commons/util/JsonUtilTest.java b/src/test/java/seedu/agendum/commons/util/JsonUtilTest.java similarity index 85% rename from src/test/java/seedu/address/commons/util/JsonUtilTest.java rename to src/test/java/seedu/agendum/commons/util/JsonUtilTest.java index fc3902188807..4e1722c23cdb 100644 --- a/src/test/java/seedu/address/commons/util/JsonUtilTest.java +++ b/src/test/java/seedu/agendum/commons/util/JsonUtilTest.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.agendum.commons.util; /** * Tests JSON Read and Write diff --git a/src/test/java/seedu/agendum/commons/util/StringUtilTest.java b/src/test/java/seedu/agendum/commons/util/StringUtilTest.java new file mode 100644 index 000000000000..fcd87e9e05b6 --- /dev/null +++ b/src/test/java/seedu/agendum/commons/util/StringUtilTest.java @@ -0,0 +1,94 @@ +package seedu.agendum.commons.util; + +import org.junit.Test; + +import java.io.FileNotFoundException; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class StringUtilTest { + + @Test + public void isUnsignedPositiveInteger() { + assertFalse(StringUtil.isUnsignedInteger(null)); + assertFalse(StringUtil.isUnsignedInteger("")); + assertFalse(StringUtil.isUnsignedInteger("a")); + assertFalse(StringUtil.isUnsignedInteger("aaa")); + assertFalse(StringUtil.isUnsignedInteger(" ")); + assertFalse(StringUtil.isUnsignedInteger("-1")); + assertFalse(StringUtil.isUnsignedInteger("0")); + assertFalse(StringUtil.isUnsignedInteger("+1")); //should be unsigned + assertFalse(StringUtil.isUnsignedInteger("-1")); //should be unsigned + assertFalse(StringUtil.isUnsignedInteger(" 10")); //should not contain whitespaces + assertFalse(StringUtil.isUnsignedInteger("10 ")); //should not contain whitespaces + assertFalse(StringUtil.isUnsignedInteger("1 0")); //should not contain whitespaces + + assertTrue(StringUtil.isUnsignedInteger("1")); + assertTrue(StringUtil.isUnsignedInteger("10")); + } + + @Test + public void getDetailsExceptionGiven(){ + assertThat(StringUtil.getDetails(new FileNotFoundException("file not found")), + containsString("java.io.FileNotFoundException: file not found")); + } + + @Test(expected = AssertionError.class) + public void getDetailsNullGivenAssertionError(){ + StringUtil.getDetails(null); + } + + //@@author A0148095X + /* + * Valid equivalence partitions for path to file: + * - file path is valid + * - file name is valid + * - file type is valid + * + * Possible scenarios returning true: + * - valid relative path to a file + * - valid absolute path to a file for windows + * - valid absolute path to a file for Unix/MacOS + * + * Possible scenarios returning false: + * - file name missing + * - file type missing + * - null path + * - empty path + * - file path not in the right format + * + * The test method below tries to verify all above with a reasonably low number of test cases. + */ + @Test + public void isValidPathToFile(){ + // null and empty file paths + assertFalse(StringUtil.isValidPathToFile(null)); // null path + assertFalse(StringUtil.isValidPathToFile("")); // empty path + + // relative file paths + assertFalse(StringUtil.isValidPathToFile("a")); // missing file type + assertFalse(StringUtil.isValidPathToFile("data/.xml")); // invalid file name + assertFalse(StringUtil.isValidPathToFile("data /valid.xml")); // invalid file path with spaces after + + assertTrue(StringUtil.isValidPathToFile("Program Files/data.xml")); // valid path to file with acceptable spaces in file path + + // absolute file paths for windows + assertFalse(StringUtil.isValidPathToFile("1:/data.xml")); // invalid drive + assertFalse(StringUtil.isValidPathToFile("C:/data/a")); // invalid file type + assertFalse(StringUtil.isValidPathToFile("C:/data/.xml")); // invalid file name + assertFalse(StringUtil.isValidPathToFile("C:/ data/valid.xml")); // invalid file path with spaces before + + assertTrue(StringUtil.isValidPathToFile("Z:/Program Files/some-other-folder/data.dat")); // valid drive, folder and file name + + // absolute file path for unix/MacOX + assertFalse(StringUtil.isValidPathToFile("/usr/data")); // invalid file type + assertFalse(StringUtil.isValidPathToFile("/usr/.xml")); // invalid file name + assertFalse(StringUtil.isValidPathToFile("/ usr/data.xml")); // invalid file path with spaces before + + assertTrue(StringUtil.isValidPathToFile("/usr/bin/my folder/data.xml")); // valid folder and file name with spaces + } + +} diff --git a/src/test/java/seedu/agendum/commons/util/XmlUtilTest.java b/src/test/java/seedu/agendum/commons/util/XmlUtilTest.java new file mode 100644 index 000000000000..37913591b72e --- /dev/null +++ b/src/test/java/seedu/agendum/commons/util/XmlUtilTest.java @@ -0,0 +1,80 @@ +package seedu.agendum.commons.util; + +import org.junit.Test; +import seedu.agendum.model.ToDoList; +import seedu.agendum.storage.XmlSerializableToDoList; +import seedu.agendum.testutil.ToDoListBuilder; +import seedu.agendum.testutil.TestUtil; + +import javax.xml.bind.JAXBException; +import java.io.File; +import java.io.FileNotFoundException; + +import static org.junit.Assert.assertEquals; + +public class XmlUtilTest { + + private static final String TEST_DATA_FOLDER = FileUtil.getPath("src/test/data/XmlUtilTest/"); + private static final File EMPTY_FILE = new File(TEST_DATA_FOLDER + "empty.xml"); + private static final File MISSING_FILE = new File(TEST_DATA_FOLDER + "missing.xml"); + private static final File VALID_FILE = new File(TEST_DATA_FOLDER + "validToDoList.xml"); + private static final File TEMP_FILE = new File(TestUtil.getFilePathInSandboxFolder("tempToDoList.xml")); + + @Test(expected = AssertionError.class) + public void getDataFromFileNullFileAssertionError() throws Exception { + XmlUtil.getDataFromFile(null, ToDoList.class); + } + + @Test(expected = AssertionError.class) + public void getDataFromFileNullClassAssertionError() throws Exception { + XmlUtil.getDataFromFile(VALID_FILE, null); + } + + @Test(expected = FileNotFoundException.class) + public void getDataFromFileMissingFileFileNotFoundException() throws Exception { + XmlUtil.getDataFromFile(MISSING_FILE, ToDoList.class); + } + + @Test(expected = JAXBException.class) + public void getDataFromFileEmptyFileDataFormatMismatchException() throws Exception { + XmlUtil.getDataFromFile(EMPTY_FILE, ToDoList.class); + } + + @Test + public void getDataFromFileValidFileValidResult() throws Exception { + XmlSerializableToDoList dataFromFile = XmlUtil.getDataFromFile(VALID_FILE, XmlSerializableToDoList.class); + assertEquals(9, dataFromFile.getTaskList().size()); + } + + @Test(expected = AssertionError.class) + public void saveDataToFileNullFileAssertionError() throws Exception { + XmlUtil.saveDataToFile(null, new ToDoList()); + } + + @Test(expected = AssertionError.class) + public void saveDataToFileNullClassAssertionError() throws Exception { + XmlUtil.saveDataToFile(VALID_FILE, null); + } + + @Test(expected = FileNotFoundException.class) + public void saveDataToFileMissingFileFileNotFoundException() throws Exception { + XmlUtil.saveDataToFile(MISSING_FILE, new ToDoList()); + } + + @Test + public void saveDataToFileValidFileDataSaved() throws Exception { + TEMP_FILE.createNewFile(); + XmlSerializableToDoList dataToWrite = new XmlSerializableToDoList(new ToDoList()); + XmlUtil.saveDataToFile(TEMP_FILE, dataToWrite); + XmlSerializableToDoList dataFromFile = XmlUtil.getDataFromFile(TEMP_FILE, XmlSerializableToDoList.class); + assertEquals((new ToDoList(dataToWrite)).toString(),(new ToDoList(dataFromFile)).toString()); + //TODO: use equality instead of string comparisons + + ToDoListBuilder builder = new ToDoListBuilder(new ToDoList()); + dataToWrite = new XmlSerializableToDoList(builder.withTask(TestUtil.generateSampleTaskData().get(0)).build()); + + XmlUtil.saveDataToFile(TEMP_FILE, dataToWrite); + dataFromFile = XmlUtil.getDataFromFile(TEMP_FILE, XmlSerializableToDoList.class); + assertEquals((new ToDoList(dataToWrite)).toString(),(new ToDoList(dataFromFile)).toString()); + } +} diff --git a/src/test/java/seedu/agendum/logic/DateTimeUtilsTest.java b/src/test/java/seedu/agendum/logic/DateTimeUtilsTest.java new file mode 100644 index 000000000000..b602fcab2545 --- /dev/null +++ b/src/test/java/seedu/agendum/logic/DateTimeUtilsTest.java @@ -0,0 +1,85 @@ +package seedu.agendum.logic; + +import org.junit.Test; +import seedu.agendum.logic.parser.DateTimeUtils; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +//@@author A0003878Y +public class DateTimeUtilsTest { + + private void assertSameDateAndTime(LocalDateTime dateTime1, LocalDateTime dateTime2) { + assertEquals(dateTime1, dateTime2); + } + + private void assertSameDate(LocalDateTime dateTime1, LocalDateTime dateTime2) { + LocalDateTime diff = dateTime1.minusHours(dateTime2.getHour()).minusMinutes(dateTime2.getMinute()).minusSeconds(dateTime2.getSecond()); + assertEquals(dateTime1, diff); + } + + @Test + public void parseNaturalLanguageDateTimeString_dateString_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("2016/01/01"); + assertSameDate(t.get(), LocalDateTime.of(2016,1,1,0,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_dateStringWith24HRTime_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("2016/01/01 01:00"); + assertSameDateAndTime(t.get(), LocalDateTime.of(2016,1,1,1,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_dateStringWithPMTime_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("2016/01/01 3pm"); + assertSameDateAndTime(t.get(), LocalDateTime.of(2016,1,1,15,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_verboseDateString_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("january 10 2017"); + assertSameDate(t.get(), LocalDateTime.of(2017,1,10,0,0)); + } + + @Test + public void parseNaturalLanguageDateTimeString_verboseDateStringWithTime_localDateTime() throws Exception { + Optional t = DateTimeUtils.parseNaturalLanguageDateTimeString("january 10 2017 5:15pm"); + assertSameDateAndTime(t.get(), LocalDateTime.of(2017,1,10,17,15)); + } + + @Test + public void balanceStartEndDateTime_startDateAfterEndDate_startDateBeforeEndDate() throws Exception { + LocalDateTime start = LocalDateTime.now(); + LocalDateTime end = start; + + start = start.plusDays(1); + end = start.plusHours(1); + + end = DateTimeUtils.balanceStartAndEndDateTime(start, end); + assertSameDateAndTime(end, start.plusHours(1)); + } + + //@@author A0148095X + @Test + public void parseNaturalLanguageDateTimeString_emptyInput_emptyOptional() { + Optional parsed = DateTimeUtils.parseNaturalLanguageDateTimeString(""); + assertFalse(parsed.isPresent()); + } + + @Test + public void parseNaturalLanguageDateTimeString_nullInput_emptyOptional() { + Optional parsed = DateTimeUtils.parseNaturalLanguageDateTimeString(null); + assertFalse(parsed.isPresent()); + } + + @Test + public void parseNaturalLanguageDateTimeString_inputNoGroups_emptyOptional() { + Optional parsed = DateTimeUtils.parseNaturalLanguageDateTimeString("asd"); + assertFalse(parsed.isPresent()); + } + +} diff --git a/src/test/java/seedu/agendum/logic/EditDistanceCalculatorTest.java b/src/test/java/seedu/agendum/logic/EditDistanceCalculatorTest.java new file mode 100644 index 000000000000..9c151dd8b81a --- /dev/null +++ b/src/test/java/seedu/agendum/logic/EditDistanceCalculatorTest.java @@ -0,0 +1,51 @@ +package seedu.agendum.logic; + +import org.junit.Test; +import seedu.agendum.logic.parser.EditDistanceCalculator; + +import java.util.Optional; + +import static org.junit.Assert.assertEquals; + +//@@author A0003878Y +public class EditDistanceCalculatorTest { + + @Test + public void closestCommandMatch_incorrectCommand_correctCommand() throws Exception { + assertEquals(EditDistanceCalculator.closestCommandMatch("adr").get(), "add"); + assertEquals(EditDistanceCalculator.closestCommandMatch("marc").get(), "mark"); + assertEquals(EditDistanceCalculator.closestCommandMatch("markk").get(), "mark"); + assertEquals(EditDistanceCalculator.closestCommandMatch("storee").get(), "store"); + assertEquals(EditDistanceCalculator.closestCommandMatch("daletr").get(), "delete"); + assertEquals(EditDistanceCalculator.closestCommandMatch("hell").get(), "help"); + assertEquals(EditDistanceCalculator.closestCommandMatch("shdule").get(), "schedule"); + assertEquals(EditDistanceCalculator.closestCommandMatch("rname").get(), "rename"); + } + + @Test + public void closestCommandMatch_incorrectCommand_invalidCommand() throws Exception { + assertEquals(EditDistanceCalculator.closestCommandMatch("asdfohasdf"), Optional.empty()); + assertEquals(EditDistanceCalculator.closestCommandMatch("poasdf"), Optional.empty()); + assertEquals(EditDistanceCalculator.closestCommandMatch("teyu6578"), Optional.empty()); + } + + @Test + public void closestCommandMatch_incompleteCommand_fullCommand() throws Exception { + assertEquals(EditDistanceCalculator.findCommandCompletion("ad").get(), "add"); + assertEquals(EditDistanceCalculator.findCommandCompletion("ma").get(), "mark"); + assertEquals(EditDistanceCalculator.findCommandCompletion("unm").get(), "unmark"); + assertEquals(EditDistanceCalculator.findCommandCompletion("und").get(), "undo"); + assertEquals(EditDistanceCalculator.findCommandCompletion("st").get(), "store"); + assertEquals(EditDistanceCalculator.findCommandCompletion("de").get(), "delete"); + assertEquals(EditDistanceCalculator.findCommandCompletion("he").get(), "help"); + assertEquals(EditDistanceCalculator.findCommandCompletion("sc").get(), "schedule"); + assertEquals(EditDistanceCalculator.findCommandCompletion("r").get(), "rename"); + } + + @Test + public void closestCommandMatch_incompleteCommand_invalidCommand() throws Exception { + assertEquals(EditDistanceCalculator.findCommandCompletion("un"), Optional.empty()); + assertEquals(EditDistanceCalculator.findCommandCompletion("iasdugfiasd"), Optional.empty()); + } + +} diff --git a/src/test/java/seedu/agendum/logic/LogicManagerTest.java b/src/test/java/seedu/agendum/logic/LogicManagerTest.java new file mode 100644 index 000000000000..0a5f8403bb29 --- /dev/null +++ b/src/test/java/seedu/agendum/logic/LogicManagerTest.java @@ -0,0 +1,1221 @@ +package seedu.agendum.logic; + +import com.google.common.eventbus.Subscribe; + +import org.junit.*; +import org.junit.rules.TemporaryFolder; + +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.core.Messages; +import seedu.agendum.commons.core.UnmodifiableObservableList; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.logic.commands.*; +import seedu.agendum.commons.events.ui.ShowHelpRequestEvent; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.commons.events.model.ChangeSaveLocationEvent; +import seedu.agendum.commons.events.model.ToDoListChangedEvent; +import seedu.agendum.model.ToDoList; +import seedu.agendum.model.Model; +import seedu.agendum.model.ModelManager; +import seedu.agendum.model.ReadOnlyToDoList; +import seedu.agendum.model.task.*; +import seedu.agendum.storage.XmlToDoListStorage; +import seedu.agendum.sync.SyncProviderGoogleTests; +import seedu.agendum.testutil.EventsCollector; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static seedu.agendum.commons.core.Messages.*; + +public class LogicManagerTest { + + /** + * See https://github.com/junit-team/junit4/wiki/rules#temporaryfolder-rule + */ + @Rule + public TemporaryFolder saveFolder = new TemporaryFolder(); + + private Model model; + private Logic logic; + + //These are for checking the correctness of the events raised + private ReadOnlyToDoList latestSavedToDoList; + private boolean helpShown; + + @Subscribe + private void handleLocalModelChangedEvent(ToDoListChangedEvent tdl) { + latestSavedToDoList = new ToDoList(tdl.data); + } + + @Subscribe + private void handleShowHelpRequestEvent(ShowHelpRequestEvent she) { + helpShown = true; + } + + @Before + public void setUp() { + model = new ModelManager(); + logic = new LogicManager(model); + EventsCenter.getInstance().registerHandler(this); + CommandLibrary.getInstance().loadAliasTable(new Hashtable()); + + latestSavedToDoList = new ToDoList(model.getToDoList()); // last saved assumed to be up to date before. + helpShown = false; + } + + @After + public void tearDown() { + EventsCenter.clearSubscribers(); + } + + @Test + public void executeInvalid() throws Exception { + String invalidCommand = " "; + assertCommandBehavior(invalidCommand, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + /** + * Executes the command and confirms that the result message is correct. + * Both the 'to do list' and the 'last shown list' are expected to be empty. + * @see #assertCommandBehavior(String, String, ReadOnlyToDoList, List) + */ + private void assertCommandBehavior(String inputCommand, String expectedMessage) throws Exception { + assertCommandBehavior(inputCommand, expectedMessage, new ToDoList(), Collections.emptyList()); + } + + /** + * Executes the command and confirms that the result message is correct and + * also confirms that the following three parts of the LogicManager object's state are as expected:
+ * - the internal to do list data are same as those in the {@code expectedToDoList}
+ * - the backing list shown by UI matches the {@code shownList}
+ * - {@code expectedToDoList} was saved to the storage file.
+ */ + private void assertCommandBehavior(String inputCommand, String expectedMessage, + ReadOnlyToDoList expectedToDoList, + List expectedShownList) throws Exception { + + // Execute the command + CommandResult result = logic.execute(inputCommand); + + // Confirm the ui display elements should contain the right data + assertEquals(expectedMessage, result.feedbackToUser); + // Generate a sorted and UnmodifiableObservableList from expectedShownList for comparison + TestDataHelper helper = new TestDataHelper(); + assertEquals(helper.generateSortedList(expectedShownList), model.getFilteredTaskList()); + + // Confirm the state of data (saved and in-memory) is as expected + assertEquals(expectedToDoList, model.getToDoList()); + assertEquals(expectedToDoList, latestSavedToDoList); + } + + + @Test + public void executeUnknownCommandWord() throws Exception { + String unknownCommand = "uicfhmowqewca"; + assertCommandBehavior(unknownCommand, MESSAGE_UNKNOWN_COMMAND); + } + + @Test + public void executeHelp() throws Exception { + assertCommandBehavior("help", HelpCommand.SHOWING_HELP_MESSAGE); + assertTrue(helpShown); + } + + @Test + public void executeExit() throws Exception { + assertCommandBehavior("exit", ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT); + } + + @Test + public void execute_add_invalidArgsFormat() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); + // no task name specified + assertCommandBehavior("add", expectedMessage, new ToDoList(), Collections.emptyList()); + assertCommandBehavior("add from 5pm to 9pm", expectedMessage, new ToDoList(), Collections.emptyList()); + // invalid date time format (only start time specified) + assertCommandBehavior("add smth from 8pm", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_addFloatingTask_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.adam(); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + + // execute command and verify result + assertCommandBehavior(helper.generateAddCommand(toBeAdded), + String.format(AddCommand.MESSAGE_SUCCESS, toBeAdded), + expectedTDL, + expectedTDL.getTaskList()); + + } + + @Test + public void execute_addDeadlineTask_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.adam(); + LocalDateTime endTime = LocalDateTime.of(2016, 10, 10, 10, 10); + toBeAdded.setEndDateTime(Optional.ofNullable(endTime)); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + + // execute command and verify result + assertCommandBehavior(helper.generateAddCommand(toBeAdded), + String.format(AddCommand.MESSAGE_SUCCESS, toBeAdded), + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_addEventTask_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.adam(); + LocalDateTime startTime = LocalDateTime.of(2016, 10, 10, 10, 10).minusHours(1); + LocalDateTime endTime = LocalDateTime.of(2016, 10, 10, 10, 10); + toBeAdded.setEndDateTime(Optional.ofNullable(startTime)); + toBeAdded.setEndDateTime(Optional.ofNullable(endTime)); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + + // execute command and verify result + assertCommandBehavior(helper.generateAddCommand(toBeAdded), + String.format(AddCommand.MESSAGE_SUCCESS, toBeAdded), + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_addEscapeDateTimeParsing() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.generateTaskWithName("drop from 21 to 1"); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + + // execute command and verify result + assertCommandBehavior("add 'drop from 21 to 1'", + String.format(AddCommand.MESSAGE_SUCCESS, toBeAdded), + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void executeAddDuplicateNotAllowed() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.adam(); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + + // setup starting state + model.addTask(toBeAdded); // task already in internal to do list + + // execute command and verify result + assertCommandBehavior( + helper.generateAddCommand(toBeAdded), + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + + } + + + @Test + public void executeListShowsAllTasks() throws Exception { + // prepare expectations + TestDataHelper helper = new TestDataHelper(); + ToDoList expectedTDL = helper.generateToDoList(2); + List expectedList = expectedTDL.getTaskList(); + + // prepare to do list state + helper.addToModel(model, 2); + + assertCommandBehavior("list", + ListCommand.MESSAGE_SUCCESS, + expectedTDL, + expectedList); + } + + + //@@author A0133367E + /** + * Confirms the 'incorrect index format behaviour' for the given command + * targeting a single task in the shown list, using visible index. + * @param commandWord to test assuming it targets a single task in the last shown list based on visible index. + * @param wordsAfterIndex contains a string that will usually follow the command + * + * This (overloaded) method is created for rename/schedule + */ + private void assertIncorrectIndexFormatBehaviorForCommand(String commandWord, String expectedMessage, String wordsAfterIndex) + throws Exception { + assertCommandBehavior(commandWord + " " + wordsAfterIndex, expectedMessage); //index missing + assertCommandBehavior(commandWord + " +1 " + wordsAfterIndex, expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " -1 " + wordsAfterIndex, expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " 0 " + wordsAfterIndex, expectedMessage); //index cannot be 0 + assertCommandBehavior(commandWord + " not_a_number " + wordsAfterIndex, expectedMessage); + } + + /** + * Confirms the 'incorrect index format behaviour' for the given command + * targeting a single/multiple task(s) in the shown list, using visible indices. + * @param commandWord to test assuming it targets a single/multiple task(s) in the shown list, using visible indices. + * + * This (overloaded) method is created for delete/mark/unmark. + */ + private void assertIncorrectIndexFormatBehaviorForCommand(String commandWord, String expectedMessage) throws Exception { + assertIncorrectIndexFormatBehaviorForCommand(commandWord, expectedMessage, " "); + + // multiple indices + assertCommandBehavior(commandWord + " +1 2 3", expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " 1 2 -3", expectedMessage); //index should be unsigned + assertCommandBehavior(commandWord + " 1 not_a_number 3 4", expectedMessage); //index cannot be a string + } + + /** + * Confirms the 'invalid argument index number behaviour' for the given command + * targeting a single task in the shown list, using visible index. + * @param commandWord to test assuming it targets a single task in the last shown list based on visible index. + * @param wordsAfterIndex contains a string that will usually follow the command + * + * This (overloaded) method is created for rename/schedule + */ + private void assertIndexNotFoundBehaviorForCommand(String commandWord, String wordsAfterIndex) throws Exception { + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(2); + + // set AB state to 2 tasks + model.resetData(new ToDoList()); + helper.addToModel(model, taskList); + + // test boundary value (one-based index is 3 when list is of size 2) + assertCommandBehavior(commandWord + " 3 " + wordsAfterIndex, MESSAGE_INVALID_TASK_DISPLAYED_INDEX, model.getToDoList(), taskList); + } + + /** + * Confirms the 'invalid argument index number behaviour' for the given command + * targeting a single/multiple task(s) in the shown list, using visible indices. + * @param commandWord to test assuming it targets tasks in the last shown list based on visible indices. + * + * This (overloaded) method is created for delete/mark/unmark. + */ + private void assertIndexNotFoundBehaviorForCommand(String commandWord) throws Exception { + assertIndexNotFoundBehaviorForCommand(commandWord, ""); + + // multiple indices + String expectedMessage = MESSAGE_INVALID_TASK_DISPLAYED_INDEX; + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(5); + + // set AB state to 5 tasks + model.resetData(new ToDoList()); + helper.addToModel(model, taskList); + + // test boundary value (one-based index is 6 when list is of size 5) + //invalid index is the last index given + assertCommandBehavior(commandWord + " 1 6", expectedMessage, model.getToDoList(), taskList); + //invalid index is not the first index + assertCommandBehavior(commandWord + " 1 6 2", expectedMessage, model.getToDoList(), taskList); + //invalid index is part of range + assertCommandBehavior(commandWord + " 1-6", expectedMessage, model.getToDoList(), taskList); + } + + + //@@author + @Test + public void execute_deleteInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("delete", expectedMessage); + } + + @Test + public void execute_deleteIndexNotFound_errorMessageShown() throws Exception { + assertIndexNotFoundBehaviorForCommand("delete"); + } + + //@@author A0133367E + @Test + public void execute_delete_removesCorrectSingleTask() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(3); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.removeTask(threeTasks.get(2)); + + // prepare model + helper.addToModel(model, threeTasks); + + // prepare message + List deletedTaskVisibleIndices = helper.generateNumberList(3); + List deletedTasks = helper.generateReadOnlyTaskList(threeTasks.get(2)); + String tasksAsString = CommandResult.tasksToString(deletedTasks, deletedTaskVisibleIndices); + + // test boundary value (last task in the list) + assertCommandBehavior("delete 3", + String.format(DeleteCommand.MESSAGE_DELETE_TASK_SUCCESS, tasksAsString), + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_delete_removesCorrectRangeOfTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.removeTask(fourTasks.get(2)); + expectedTDL.removeTask(fourTasks.get(1)); + + // prepare model + helper.addToModel(model, fourTasks); + + //prepare message + List deletedTaskVisibleIndices = helper.generateNumberList(2, 3); + List deletedTasks = helper.generateReadOnlyTaskList(fourTasks.get(1), fourTasks.get(2)); + String tasksAsString = CommandResult.tasksToString(deletedTasks, deletedTaskVisibleIndices); + + // Delete tasks with visible index in range [startIndex, endIndex] = [2, 3] + // Checks if the new to do list contains Task 1 and Task 4 from the last visible list + assertCommandBehavior("delete 2-3", + String.format(DeleteCommand.MESSAGE_DELETE_TASK_SUCCESS, tasksAsString), + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_delete_removesCorrectMultipleTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.removeTask(fourTasks.get(3)); + expectedTDL.removeTask(fourTasks.get(2)); + expectedTDL.removeTask(fourTasks.get(1)); + + // prepare model + helper.addToModel(model, fourTasks); + + // prepare message + List deletedTaskVisibleIndices = helper.generateNumberList(2, 3, 4); + List deletedTasks = helper.generateReadOnlyTaskList( + fourTasks.get(1), fourTasks.get(2), fourTasks.get(3)); + String tasksAsString = CommandResult.tasksToString(deletedTasks, deletedTaskVisibleIndices); + + assertCommandBehavior("delete 2,3 4", + String.format(DeleteCommand.MESSAGE_DELETE_TASK_SUCCESS, tasksAsString), + expectedTDL, + expectedTDL.getTaskList()); + } + //@author + + @Test + public void execute_syncOn_successfull() throws Exception { + SyncProviderGoogleTests.copyTestCredentials(); + assertCommandBehavior("sync on", + SyncCommand.SYNC_ON_MESSAGE); + + SyncProviderGoogleTests.deleteCredential(); // Clean up credential file + } + + @Test + public void execute_syncOff_successfull() throws Exception { + assertCommandBehavior("sync off", + SyncCommand.SYNC_OFF_MESSAGE); + } + + @Test + public void execute_syncUnknown_exception() throws Exception { + assertCommandBehavior("sync something", SyncCommand.MESSAGE_WRONG_OPTION, new ToDoList(), Collections.emptyList()); + } + + + //@@author A0148095X + @Test + public void execute_store_successful() throws Exception { + // setup expectations + ToDoList expectedTDL = new ToDoList(); + Task testTask = new Task(new Name("test_store")); + expectedTDL.addTask(testTask); + model.addTask(testTask); + + String location = "data/test_store_successful.xml"; + CommandResult result; + String inputCommand; + String feedback; + EventsCollector eventCollector = new EventsCollector(); + + // execute command and verify result + inputCommand = "store " + location; + result = logic.execute(inputCommand); + feedback = String.format(StoreCommand.MESSAGE_SUCCESS, location); + assertEquals(feedback, result.feedbackToUser); + assertTrue(eventCollector.get(0) instanceof ChangeSaveLocationEvent); + assertTrue(eventCollector.get(1) instanceof ToDoListChangedEvent); + + // execute command and verify result + inputCommand = "store default"; + result = logic.execute(inputCommand); + feedback = String.format(StoreCommand.MESSAGE_LOCATION_DEFAULT, Config.DEFAULT_SAVE_LOCATION); + assertEquals(feedback, result.feedbackToUser); + assertTrue(eventCollector.get(2) instanceof ChangeSaveLocationEvent); + assertTrue(eventCollector.get(3) instanceof ToDoListChangedEvent); + } + + @Test + public void execute_store_fileExists_fail() throws Exception { + // setup expectations + ToDoList expectedTDL = new ToDoList(); + String location = "data/test_store_fail.xml"; + + // create file + FileUtil.createIfMissing(new File(location)); + + // error that file already exists + assertCommandBehavior("store " + location, + String.format(StoreCommand.MESSAGE_FILE_EXISTS, location), + expectedTDL, + expectedTDL.getTaskList()); + + // delete file + FileUtil.deleteFile(location); + } + + @Test + public void execute_exit_success() { + CommandResult result = logic.execute(ExitCommand.COMMAND_WORD); + assertEquals(ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT, result.feedbackToUser); + } + //@@author + + //@@author A0133367E + @Test + public void execute_markInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("mark", expectedMessage); + } + + @Test + public void execute_markIndexNotFound_errorMessageShown() throws Exception { + assertIndexNotFoundBehaviorForCommand("mark"); + } + + @Test + public void execute_markToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List tasks = helper.generateTaskList(4); + tasks.add(helper.generateCompletedTask(2)); + + ToDoList expectedTDL = helper.generateToDoList(tasks); + + helper.addToModel(model, tasks); + + assertCommandBehavior("mark 1-3", + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_mark_marksCorrectSingleTaskAsCompleted() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(3); + + // prepared expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.markTask(threeTasks.get(0)); + + // prepare model + helper.addToModel(model, threeTasks); + + // test boundary value (first task in the list) + assertCommandBehavior("mark 1", + MarkCommand.MESSAGE_MARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_mark_marksCorrectRangeOfTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.markTask(fourTasks.get(2)); + expectedTDL.markTask(fourTasks.get(3)); + + // prepare model + helper.addToModel(model, fourTasks); + + // test boundary value (up to last task in the list) + assertCommandBehavior("mark 3-4", + MarkCommand.MESSAGE_MARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_mark_marksCorrectMultipleTasks() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateTaskList(4); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.markTask(fourTasks.get(1)); + expectedTDL.markTask(fourTasks.get(2)); + expectedTDL.markTask(fourTasks.get(3)); + + // prepare model + helper.addToModel(model, fourTasks); + + assertCommandBehavior("mark 2,3 4", + MarkCommand.MESSAGE_MARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_unmarkInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnmarkCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("unmark", expectedMessage); + } + + @Test + public void execute_unmarkIndexNotFound_errorMessageShown() throws Exception { + assertIndexNotFoundBehaviorForCommand("unmark"); + } + + @Test + public void execute_unmarkToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List tasks = helper.generateCompletedTaskList(4); + tasks.add(helper.generateTask(2)); + + ToDoList expectedTDL = helper.generateToDoList(tasks); + + helper.addToModel(model, tasks); + + assertCommandBehavior("unmark 3-5", + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_unmark_UnmarksCorrectSingleTaskFromCompleted() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(2); + threeTasks.add(helper.generateCompletedTask(3)); + + // prepare expectedTDL - does not have any tasks marked as completed + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.unmarkTask(threeTasks.get(2)); + + // prepare model + helper.addToModel(model, threeTasks); + + // test boundary value - last task in the list + assertCommandBehavior("unmark 3", + UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_unmark_unmarksCorrectRangeOfTasks() throws Exception { + // indexes provided are startIndex-endIndex. + // Tasks with visible index in range [startIndex, endIndex] are marked + TestDataHelper helper = new TestDataHelper(); + List fourCompletedTasks = helper.generateCompletedTaskList(4); + List fourTasks = helper.generateTaskList(4); + + // prepare expectedTDL - does not have any tasks marked as completed + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + + // prepare model + helper.addToModel(model, fourCompletedTasks); + + assertCommandBehavior("unmark 1-4", + UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_unmark_unmarksCorrectMultipleTasks() throws Exception { + // unmark multiple indices specified (separated by space/comma) + TestDataHelper helper = new TestDataHelper(); + List fourTasks = helper.generateCompletedTaskList(4); + + // prepare expectedTDL + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + expectedTDL.unmarkTask(fourTasks.get(3)); + expectedTDL.unmarkTask(fourTasks.get(2)); + expectedTDL.unmarkTask(fourTasks.get(1)); + + // prepare model + helper.addToModel(model, fourTasks); + + assertCommandBehavior("unmark 2,3 4", + String.format(UnmarkCommand.MESSAGE_UNMARK_TASK_SUCCESS), + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_renameInvalidArgsFormat_errorMessageShown() throws Exception { + // invalid index format + // a valid name is provided since invalid input values must be tested one at a time + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, RenameCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("rename", expectedMessage, " new task name"); + + // invalid new task name format e.g. task name is not provided + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(2); + + helper.addToModel(model, taskList); + + // a valid index is provided since we are testing for invalid name (empty string) here + assertCommandBehavior("rename 1 ", expectedMessage, model.getToDoList(), taskList); + + } + + @Test + public void execute_renameIndexNotFound_errorMessageShown() throws Exception { + // a valid name is provided to only test for invalid index + assertIndexNotFoundBehaviorForCommand("rename", " new task name"); + } + + @Test + public void execute_renameToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task toBeDuplicated = helper.adam(); + Task toBeRenamed = helper.generateTask(1); + List twoTasks = helper.generateTaskList(toBeDuplicated, toBeRenamed); + ToDoList expectedTDL = helper.generateToDoList(twoTasks); + + helper.addToModel(model, twoTasks); + + // execute command and verify result + // a valid index must be provided to check if the name is invalid (due to a duplicate) + assertCommandBehavior( + "rename 2 " + toBeDuplicated.getName().toString(), + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_rename_RenamesCorrectTask() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(2); + Task taskToRename = helper.generateCompletedTask(3); + threeTasks.add(taskToRename); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + Task renamedTask = new Task(taskToRename); + String newTaskName = "a brand new task name"; + renamedTask.setName(new Name(newTaskName)); + expectedTDL.updateTask(taskToRename, renamedTask); + + // prepare model + helper.addToModel(model, threeTasks); + + // boundary value: use the last task + assertCommandBehavior("rename 3 " + newTaskName, + String.format(RenameCommand.MESSAGE_SUCCESS, newTaskName), + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_scheduleInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE); + + // no index and time are provided + assertCommandBehavior("schedule", expectedMessage, new ToDoList(), Collections.emptyList()); + + // invalid index format + // a valid time is provided since invalid input values must be tested one at a time + assertIncorrectIndexFormatBehaviorForCommand("schedule", expectedMessage, "by 9pm"); + + // invalid time format provided + TestDataHelper helper = new TestDataHelper(); + List taskList = helper.generateTaskList(2); + helper.addToModel(model, taskList); + + // a valid index is provided since we are testing for invalid time format here + assertCommandBehavior("schedule 1 blue", expectedMessage, model.getToDoList(), taskList); + assertCommandBehavior("schedule 1 from 7pm", expectedMessage, model.getToDoList(), taskList); + } + + @Test + public void execute_scheduleIndexNotFound_errorMessageShown() throws Exception { + // a valid time is provided to only test for invalid index + assertIndexNotFoundBehaviorForCommand("schedule", "by 9pm"); + } + + @Test + public void execute_scheduleToGetDuplicate_notAllowed() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task toBeDuplicated = helper.generateTask(1); + LocalDateTime time = LocalDateTime.of(2016, 10, 10, 10, 10); + toBeDuplicated.setEndDateTime(Optional.ofNullable(time)); + Task toBeScheduled = helper.generateTask(1); + List twoTasks = helper.generateTaskList(toBeDuplicated, toBeScheduled); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(twoTasks); + + // prepare model + model.resetData(expectedTDL); + + // execute command and verify result + // a valid index must be provided to check if the time is invalid (due to a duplicate) + assertCommandBehavior( + "schedule 2 by Oct 10 10:10", + Messages.MESSAGE_DUPLICATE_TASK, + expectedTDL, + expectedTDL.getTaskList()); + } + + @Test + public void execute_schedule_scheduleCorrectTask() throws Exception { + TestDataHelper helper = new TestDataHelper(); + List threeTasks = helper.generateTaskList(2); + + Task floatingTask = helper.generateTask(3); + threeTasks.add(floatingTask); + + LocalDateTime endTime = LocalDateTime.of(2016, 10, 10, 10, 10); + LocalDateTime startTime = LocalDateTime.of(2016, 9, 9, 9, 10); + Task eventTask = helper.generateTask(3); + eventTask.setStartDateTime(Optional.ofNullable(startTime)); + eventTask.setEndDateTime(Optional.ofNullable(endTime)); + + // prepare expected TDL + ToDoList expectedTDL = helper.generateToDoList(threeTasks); + expectedTDL.updateTask(floatingTask, eventTask); + + // prepare model + helper.addToModel(model, threeTasks); + + assertCommandBehavior("schedule 3 from Sep 9 9:10 to Oct 10 10:10", + String.format(ScheduleCommand.MESSAGE_SUCCESS, eventTask), + expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_aliasInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AliasCommand.MESSAGE_USAGE); + assertCommandBehavior("alias", expectedMessage, new ToDoList(), Collections.emptyList()); + // alias should not contain symbols + assertCommandBehavior("alias add +", expectedMessage, new ToDoList(), Collections.emptyList()); + // new alias key has space + assertCommandBehavior("alias add a 1", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasNonOriginalCommand_errorMessageShown() throws Exception { + String expectedMessage = String.format(AliasCommand.MESSAGE_FAILURE_NON_ORIGINAL_COMMAND, "a"); + assertCommandBehavior("alias a short", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasKeyIsReservedCommandWord_errorMessageShown() throws Exception { + String expectedMessage = String.format(AliasCommand.MESSAGE_FAILURE_RESERVED_COMMAND_WORD, + RenameCommand.COMMAND_WORD); + String userCommand = "alias " + AddCommand.COMMAND_WORD + " " + RenameCommand.COMMAND_WORD; + assertCommandBehavior(userCommand, expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasKeyIsInUse_errorMessageShown() throws Exception { + String expectedMessage = String.format(AliasCommand.MESSAGE_FAILURE_ALIAS_IN_USE, + "r", RenameCommand.COMMAND_WORD); + CommandLibrary.getInstance().addNewAlias("r", RenameCommand.COMMAND_WORD); + String userCommand = "alias " + AddCommand.COMMAND_WORD + " r"; + assertCommandBehavior(userCommand, expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_aliasValidCaseInsensitiveKey_successfullyAdded() throws Exception { + // successfully add alias + String expectedMessage = String.format(AliasCommand.MESSAGE_SUCCESS, "a", AddCommand.COMMAND_WORD); + + assertCommandBehavior("alias " + AddCommand.COMMAND_WORD + " A", + expectedMessage, new ToDoList(), Collections.emptyList()); + + // alias can be used + TestDataHelper helper = new TestDataHelper(); + + Task addedTask = helper.generateTaskWithName("new task"); + List taskList = helper.generateTaskList(addedTask); + + ToDoList expectedTDL = helper.generateToDoList(taskList); + expectedMessage = String.format(AddCommand.MESSAGE_SUCCESS, addedTask); + + assertCommandBehavior("a new task", expectedMessage, expectedTDL, + expectedTDL.getTaskList()); + } + + + @Test + public void execute_unaliasInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnaliasCommand.MESSAGE_USAGE); + assertCommandBehavior("unalias", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_unaliasReservedCommandWord_errorMessageShown() throws Exception { + String expectedMessage = String.format(UnaliasCommand.MESSAGE_FAILURE_RESERVED_COMMAND_WORD, + AddCommand.COMMAND_WORD); + String command = "unalias " + AddCommand.COMMAND_WORD; + assertCommandBehavior(command, expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_unaliasNoSuchKey_errorMessageShown() throws Exception { + String expectedMessage = String.format(UnaliasCommand.MESSAGE_FAILURE_NO_ALIAS_KEY, "smth"); + assertCommandBehavior("unalias smth", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_unaliasExistingAliasKey_successfullyRemoved() throws Exception { + // successfully remove alias + String expectedMessage = String.format(UnaliasCommand.MESSAGE_SUCCESS, "wow"); + CommandLibrary.getInstance().addNewAlias("wow", AddCommand.COMMAND_WORD); + assertCommandBehavior("unalias wow", expectedMessage, new ToDoList(), Collections.emptyList()); + + // previous alias key cannot be used + expectedMessage = MESSAGE_UNKNOWN_COMMAND; + assertCommandBehavior("wow new task", expectedMessage, new ToDoList(), Collections.emptyList()); + } + + + //@@author + @Test + public void execute_find_invalidArgsFormat() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE); + assertCommandBehavior("find ", expectedMessage); + } + + @Test + public void executeFindOnlyMatchesFullWordsInNames() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task pTarget1 = helper.generateTaskWithName("bla bla KEY bla"); + Task pTarget2 = helper.generateTaskWithName("bla KEY bla bceofeia"); + Task p1 = helper.generateTaskWithName("KE Y"); + Task p2 = helper.generateTaskWithName("KEYKEYKEY sduauo"); + + List fourTasks = helper.generateTaskList(p1, pTarget1, p2, pTarget2); + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + List expectedList = helper.generateTaskList(pTarget1, pTarget2); + helper.addToModel(model, fourTasks); + + assertCommandBehavior("find KEY", + Command.getMessageForTaskListShownSummary(expectedList.size()), + expectedTDL, + expectedList); + } + + @Test + public void executeFindIsNotCaseSensitive() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task p1 = helper.generateTaskWithName("bla bla KEY bla"); + Task p2 = helper.generateTaskWithName("bla KEY bla bceofeia"); + Task p3 = helper.generateTaskWithName("key key"); + Task p4 = helper.generateTaskWithName("KEy sduauo"); + + List fourTasks = helper.generateTaskList(p3, p1, p4, p2); + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + helper.addToModel(model, fourTasks); + + assertCommandBehavior("find KEY", + Command.getMessageForTaskListShownSummary(fourTasks.size()), + expectedTDL, + fourTasks); + } + + @Test + public void executeFindMatchesIfAnyKeywordPresent() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task pTarget1 = helper.generateTaskWithName("bla bla KEY bla"); + Task pTarget2 = helper.generateTaskWithName("bla rAnDoM bla bceofeia"); + Task pTarget3 = helper.generateTaskWithName("key key"); + Task p1 = helper.generateTaskWithName("sduauo"); + + List fourTasks = helper.generateTaskList(pTarget1, p1, pTarget2, pTarget3); + ToDoList expectedTDL = helper.generateToDoList(fourTasks); + List expectedList = helper.generateTaskList(pTarget1, pTarget2, pTarget3); + helper.addToModel(model, fourTasks); + + assertCommandBehavior("find key rAnDoM", + Command.getMessageForTaskListShownSummary(expectedList.size()), + expectedTDL, + expectedList); + } + + //@@author A0133367E + @Test + public void execute_undo_identifiesNoPreviousChanges() throws Exception { + assertCommandBehavior("undo", UndoCommand.MESSAGE_FAILURE, new ToDoList(), Collections.emptyList()); + } + + @Test + public void execute_undo_reversePreviousChangeToTaskList() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task p1 = helper.generateTaskWithName("old name"); + List listWithOneTask = helper.generateTaskList(p1); + ToDoList expectedTDL = helper.generateToDoList(listWithOneTask); + List readOnlyTaskList = helper.generateReadOnlyTaskList(p1); + + // Undo add command + model.addTask(p1); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, new ToDoList(), Collections.emptyList()); + + // Undo delete command + model.addTask(p1); + model.deleteTasks(readOnlyTaskList); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, expectedTDL, listWithOneTask); + + // Undo rename command + Task p2 = new Task(p1); + p2.setName(new Name("new name")); + model.updateTask(p1, p2); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, expectedTDL, listWithOneTask); + + // Undo mark command + model.markTasks(readOnlyTaskList); + assertCommandBehavior("undo", UndoCommand.MESSAGE_SUCCESS, expectedTDL, listWithOneTask); + } + + + //@@author A0148095X + @Test + public void executeLoad_fileExists_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.generateTask(999); + ToDoList expectedTDL = new ToDoList(); + expectedTDL.addTask(toBeAdded); + model.addTask(toBeAdded); + + // setup storage file + String filePath = "data/test/load.xml"; + XmlToDoListStorage xmltdls = new XmlToDoListStorage(filePath); + xmltdls.saveToDoList(expectedTDL); + + // execute command and verify result + assertCommandBehavior("load " + filePath, + String.format(LoadCommand.MESSAGE_SUCCESS, filePath), + expectedTDL, + expectedTDL.getTaskList()); + + FileUtil.deleteFile(filePath); + } + + @Test + public void executeLoad_fileDoesNotExist_fail() throws Exception { + // setup expectations + ToDoList expectedTDL = new ToDoList(); + String filePath = "data/test/loadDoesNotExist.xml"; + + // execute command and verify result + assertCommandBehavior("load " + filePath, + String.format(LoadCommand.MESSAGE_FILE_DOES_NOT_EXIST, filePath), + expectedTDL, + expectedTDL.getTaskList()); + } + //@@author + + /** + * A utility class to generate test data. + */ + class TestDataHelper{ + + private LocalDateTime fixedTime = LocalDateTime.of(2016, 10, 10, 10, 10); + private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd HH:mm"); + + private Task adam() throws Exception { + Name name = new Name("Adam Brown"); + Task adam = new Task(name); + adam.setLastUpdatedTime(fixedTime); + return adam; + } + + /** + * Generates a valid task using the given seed. + * Running this function with the same parameter values guarantees the returned task will have the same state. + * Each unique seed will generate a unique Task object. + * + * @param seed used to generate the task data field values + */ + private Task generateTask(int seed) throws Exception { + Task task = new Task( + new Name("Task " + seed) + ); + task.setLastUpdatedTime(fixedTime); + return task; + } + + /** + * Generates a valid completed task with the given seed + */ + private Task generateCompletedTask(int seed) throws Exception { + Task newTask = generateTask(seed); + newTask.markAsCompleted(); + newTask.setLastUpdatedTime(fixedTime); + return newTask; + } + + /** + * Generates a Task object with given name. Other fields will have some dummy values. + */ + private Task generateTaskWithName(String name) throws Exception { + Task namedTask = new Task( + new Name(name) + ); + namedTask.setLastUpdatedTime(fixedTime); + return namedTask; + } + + /** Generates the correct add command based on the task given */ + private String generateAddCommand(Task task) { + StringBuilder command = new StringBuilder(); + command.append("add " + task.getName().toString() + " "); + if (task.isEvent()) { + command.append(" from "); + command.append(task.getStartDateTime().get().format(formatter)); + command.append(" to "); + command.append(task.getEndDateTime().get().format(formatter)); + } else if (task.hasDeadline()) { + command.append(" by "); + command.append(task.getEndDateTime().get().format(formatter)); + } + return command.toString(); + } + + /** + * Generates an ToDoList with auto-generated tasks. + */ + private ToDoList generateToDoList(int numGenerated) throws Exception{ + ToDoList toDoList = new ToDoList(); + addToToDoList(toDoList, numGenerated); + return toDoList; + } + + /** + * Generates an ToDoList based on the list of Tasks given. + */ + private ToDoList generateToDoList(List tasks) throws Exception{ + ToDoList toDoList = new ToDoList(); + addToToDoList(toDoList, tasks); + return toDoList; + } + + /** + * Adds auto-generated Task objects to the given ToDoList + * @param toDoList The ToDoList to which the Tasks will be added + */ + private void addToToDoList(ToDoList toDoList, int numGenerated) throws Exception{ + addToToDoList(toDoList, generateTaskList(numGenerated)); + } + + /** + * Adds the given list of Tasks to the given ToDoList + */ + private void addToToDoList(ToDoList toDoList, List tasksToAdd) throws Exception{ + for(Task p: tasksToAdd){ + toDoList.addTask(p); + } + } + + /** + * Adds auto-generated Task objects to the given model + * @param model The model to which the Tasks will be added + */ + private void addToModel(Model model, int numGenerated) throws Exception{ + addToModel(model, generateTaskList(numGenerated)); + } + + /** + * Adds the given list of Tasks to the given model + */ + private void addToModel(Model model, List tasksToAdd) throws Exception{ + for(Task p: tasksToAdd){ + model.addTask(p); + } + } + + /** + * Generates a list of uncompleted tasks + */ + private List generateTaskList(int numGenerated) throws Exception{ + List tasks = new ArrayList<>(); + for(int i = 1; i <= numGenerated; i++){ + tasks.add(generateTask(i)); + } + return tasks; + } + + /** + * Generates a list of completed tasks + */ + private List generateCompletedTaskList(int numGenerated) throws Exception{ + List tasks = new ArrayList<>(); + for(int i = 1; i <= numGenerated; i++){ + tasks.add(generateCompletedTask(i)); + } + return tasks; + } + + private List generateTaskList(Task... tasks) { + return Arrays.asList(tasks); + } + + private List generateReadOnlyTaskList(ReadOnlyTask... tasks) { + return Arrays.asList(tasks); + } + + private List generateNumberList(Integer... numbers){ + return Arrays.asList(numbers); + } + + /** + * Generate a sorted UnmodifiableObservableList from expectedShownList + */ + private UnmodifiableObservableList generateSortedList(List expectedShownList) throws Exception { + List taskList = expectedShownList.stream().map((Function) Task::new).collect(Collectors.toList()); + ToDoList toDoList = generateToDoList(taskList); + return new UnmodifiableObservableList<>(toDoList.getTasks().sorted()); + } + + } +} diff --git a/src/test/java/seedu/agendum/logic/commands/CommandTest.java b/src/test/java/seedu/agendum/logic/commands/CommandTest.java new file mode 100644 index 000000000000..a0ac1bc53f8b --- /dev/null +++ b/src/test/java/seedu/agendum/logic/commands/CommandTest.java @@ -0,0 +1,28 @@ +package seedu.agendum.logic.commands; + +import org.junit.Test; +import seedu.agendum.commons.core.Messages; + +import static org.junit.Assert.*; + +public class CommandTest { + @Test + public void getName() { + assertNull(Command.getName()); + } + + @Test + public void getFormat() { + assertNull(Command.getFormat()); + } + + @Test + public void getDescription() { + assertNull(Command.getDescription()); + } + + @Test + public void getMessageForTaskListShownSummary() { + assertEquals(Command.getMessageForTaskListShownSummary(100), String.format(Messages.MESSAGE_TASKS_LISTED_OVERVIEW, 100)); + } +} \ No newline at end of file diff --git a/src/test/java/seedu/agendum/logic/commands/IncorrectCommandTest.java b/src/test/java/seedu/agendum/logic/commands/IncorrectCommandTest.java new file mode 100644 index 000000000000..5bb9a9a638ec --- /dev/null +++ b/src/test/java/seedu/agendum/logic/commands/IncorrectCommandTest.java @@ -0,0 +1,23 @@ +package seedu.agendum.logic.commands; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class IncorrectCommandTest { + @Test + public void getName() { + assertNull(IncorrectCommand.getName()); + } + + @Test + public void getFormat() { + assertNull(IncorrectCommand.getFormat()); + } + + @Test + public void getDescription() { + assertNull(IncorrectCommand.getDescription()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/agendum/model/NameTest.java b/src/test/java/seedu/agendum/model/NameTest.java new file mode 100644 index 000000000000..05b14c0680fb --- /dev/null +++ b/src/test/java/seedu/agendum/model/NameTest.java @@ -0,0 +1,40 @@ +package seedu.agendum.model; + +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.Name; + +//@@author A0148095X +public class NameTest { + private String invalidNameString = "Vishnu \n Rachael \n Weigang"; + private String validNameString = "Justin"; + + private Name one; + private Name another; + + @Before + public void setUp() throws IllegalValueException { + one = new Name(validNameString); + another = new Name(validNameString); + } + + @Test + public void equals_symmetric_returnsTrue() throws IllegalValueException { + assertTrue(one.equals(another) && another.equals(one)); + } + + @Test + public void hashCode_symmetric_returnsTrue() throws IllegalValueException { + assertTrue(one.hashCode() == another.hashCode()); + } + + @SuppressWarnings("unused") + @Test (expected = IllegalValueException.class) + public void name_invalid_throwsIllegalValueException() throws IllegalValueException { + Name name = new Name(invalidNameString); + } +} diff --git a/src/test/java/seedu/agendum/model/TaskTest.java b/src/test/java/seedu/agendum/model/TaskTest.java new file mode 100644 index 000000000000..6aaeaad87da8 --- /dev/null +++ b/src/test/java/seedu/agendum/model/TaskTest.java @@ -0,0 +1,254 @@ +package seedu.agendum.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.model.task.Name; +import seedu.agendum.model.task.Task; + +//@@author A0133367E +public class TaskTest { + + private Task floatingTask; + private Task eventTask; + private Task deadlineTaskDueYesterday; + private Task deadlineTaskDueTomorrow; + private Optional noTimeSpecified = Optional.empty(); + private Optional yesterday = Optional.ofNullable(LocalDateTime.now().minusDays(1)); + private Optional tomorrow = Optional.ofNullable(LocalDateTime.now().plusDays(1)); + + /** + * Convenient helper method to generate a task with Name name, + * the specified completion status, start and end time (specified by optional). + * Last updated time is set to yesterday. + */ + private Task generateTask(String name, boolean isCompleted, Optional startTime, + Optional endTime) throws Exception { + Name taskName = new Name(name); + Task task = new Task(taskName, startTime, endTime); + if (isCompleted) { + task.markAsCompleted(); + } + task.setLastUpdatedTime(yesterday.get()); + return task; + } + + @Before + public void setUp() throws Exception { + floatingTask = generateTask("task", false, noTimeSpecified, noTimeSpecified); + eventTask = generateTask("task", false, yesterday, tomorrow); + deadlineTaskDueYesterday = generateTask("task", false, noTimeSpecified, yesterday); + deadlineTaskDueTomorrow = generateTask("task", false, noTimeSpecified, tomorrow); + } + + @Test + public void isOverdue_floatingTask_returnsFalse() { + assertFalse(floatingTask.isOverdue()); + } + + @Test + public void isOverdue_completedTask_returnsFalse() { + // testing for completion status, give valid (overdue) end date time + deadlineTaskDueYesterday.markAsCompleted(); + assertFalse(deadlineTaskDueYesterday.isOverdue()); + } + + @Test + public void isOverdue_uncompletedFutureTask_returnsFalse() { + assertFalse(deadlineTaskDueTomorrow.isOverdue()); + } + + @Test + public void isOverdue_uncompletedTaskFromYesterday_returnsTrue() { + assertTrue(deadlineTaskDueYesterday.isOverdue()); + assertTrue(eventTask.isOverdue()); + } + + @Test + public void isUpcoming_floatingTask_returnsFalse() { + assertFalse(floatingTask.isUpcoming()); + } + + @Test + public void isUpcoming_completedTask_returnsFalse() { + // testing for completion status, give valid (upcoming) end date time + deadlineTaskDueTomorrow.markAsCompleted(); + assertFalse(deadlineTaskDueTomorrow.isUpcoming()); + } + + @Test + public void isUpcoming_taskWithEndTimeYesterday_returnsFalse() { + assertFalse(deadlineTaskDueYesterday.isUpcoming()); + } + + @Test + public void isUpcoming_uncompletedTaskFromNextMonth_returnsFalse() { + LocalDateTime nextMonth = LocalDateTime.now().plusMonths(1); + floatingTask.setEndDateTime(Optional.ofNullable(nextMonth)); + assertFalse(floatingTask.isUpcoming()); + } + + @Test + public void isUpcoming_uncompletedTaskFromTomorrow_returnsTrue() { + assertTrue(deadlineTaskDueTomorrow.isUpcoming()); + } + + @Test + public void isEvent_floatingTask_returnsFalse() { + assertFalse(floatingTask.isEvent()); + } + + @Test + public void isEvent_taskWithNoStartTime_returnsFalse() { + assertFalse(deadlineTaskDueTomorrow.isEvent()); + } + + @Test + public void isEvent_taskHasStartAndEndTime_returnsTrue() { + assertTrue(eventTask.isEvent()); + } + + @Test + public void hasDeadline_floatingTask_returnsFalse() { + assertFalse(floatingTask.hasDeadline()); + } + + @Test + public void hasDeadline_taskWithEndTimeButNoStartTime_returnsTrue() { + assertTrue(deadlineTaskDueTomorrow.hasDeadline()); + } + + @Test + public void hasDeadline_taskHasStartAndEndTime_returnsFalse() { + assertFalse(eventTask.hasDeadline()); + } + + @Test + public void setName_updateNameAndPreserveProperties() throws Exception { + Task copiedTask = new Task(eventTask); + eventTask.setName(new Name("updated task")); + + assertEquals(eventTask.getName().toString(), "updated task"); + assertEquals(eventTask.getStartDateTime(), copiedTask.getStartDateTime()); + assertEquals(eventTask.getEndDateTime(), copiedTask.getEndDateTime()); + assertEquals(eventTask.isCompleted(), copiedTask.isCompleted()); + // should change the last updated time + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void setStartTime_updateStartTimeAndPreserveProperties() { + Task copiedTask = new Task(eventTask); + eventTask.setStartDateTime(Optional.empty()); + + assertEquals(eventTask.getStartDateTime(), Optional.empty()); + assertEquals(eventTask.getName(), copiedTask.getName()); + assertEquals(eventTask.getEndDateTime(), copiedTask.getEndDateTime()); + assertEquals(eventTask.isCompleted(), copiedTask.isCompleted()); + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void setEndTime_updateEndTimeAndPreserveProperties() { + Task copiedTask = new Task(eventTask); + eventTask.setEndDateTime(yesterday); + + assertEquals(eventTask.getEndDateTime(), yesterday); + assertEquals(eventTask.getName(), copiedTask.getName()); + assertEquals(eventTask.getStartDateTime(), copiedTask.getStartDateTime()); + assertEquals(eventTask.isCompleted(), copiedTask.isCompleted()); + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void markAsCompleted_markAsCompletedAndPreserveProperties() { + Task copiedTask = new Task(eventTask); + eventTask.markAsCompleted(); + + assertTrue(eventTask.isCompleted()); + assertEquals(eventTask.getName(), copiedTask.getName()); + assertEquals(eventTask.getStartDateTime(), copiedTask.getStartDateTime()); + assertEquals(eventTask.getEndDateTime(), copiedTask.getEndDateTime()); + assertTrue(eventTask.getLastUpdatedTime() + .isAfter(copiedTask.getLastUpdatedTime())); + } + + @Test + public void equals_tasksWithOnlyDifferentName_returnsFalse() throws Exception { + Task anotherTask = generateTask("new task", false, noTimeSpecified, noTimeSpecified); + assertFalse(floatingTask.equals(anotherTask)); + } + + @Test + public void equals_tasksWithOnlyDifferentCompletionStatus_returnsFalse() throws Exception { + Task anotherTask = generateTask("task", true, noTimeSpecified, noTimeSpecified); + assertFalse(floatingTask.equals(anotherTask)); + } + + @Test + public void equals_tasksWithOnlyDifferentTaskTime_returnsFalse() throws Exception { + assertFalse(deadlineTaskDueTomorrow.equals(deadlineTaskDueYesterday)); + } + + @Test + public void equals_tasksWithOnlyDifferentUpdatedTime_returnsTrue() throws Exception { + Task copiedTask = new Task(floatingTask); + copiedTask.setLastUpdatedTimeToNow(); + assertEquals(floatingTask, copiedTask); + } + + @Test + public void compareTo_uncompletedAndCompletedTasks_uncompletedFirst() throws Exception { + // tasks with same time + Task completedTask = generateTask("task", true, noTimeSpecified, noTimeSpecified); + assertTrue(floatingTask.compareTo(completedTask) < 0); + + // tasks with different end time and updated time + deadlineTaskDueYesterday.markAsCompleted(); + assertTrue(floatingTask.compareTo(deadlineTaskDueYesterday) < 0); + } + + /** + * Compare uncompleted tasks based on their task time (start time if present, else end time) + */ + @Test + public void compareTo_uncompletedTasks_earlierAndPresentTaskTimeFirst() { + assertTrue(deadlineTaskDueYesterday.compareTo(deadlineTaskDueTomorrow) < 0); + assertTrue(deadlineTaskDueTomorrow.compareTo(floatingTask) < 0); + assertTrue(floatingTask.compareTo(deadlineTaskDueYesterday) > 0); + } + + /** + * Compare completed tasks based on their last updated time + * (Regardless of the start and end time associated with the task) + */ + @Test + public void compareTo_completedTasks_laterUpdatedTimeFirst() { + + deadlineTaskDueTomorrow.markAsCompleted(); + deadlineTaskDueTomorrow.setLastUpdatedTime(yesterday.get()); + + // have later updated time + deadlineTaskDueYesterday.markAsCompleted(); + deadlineTaskDueYesterday.setLastUpdatedTime(tomorrow.get()); + + assertTrue(deadlineTaskDueYesterday.compareTo(deadlineTaskDueTomorrow) < 0); + } + + @Test + public void compareTo_tasksWithOnlyDifferentNames_lexicographicalOrder() throws Exception { + Task anotherTask = generateTask("another task", false, noTimeSpecified, noTimeSpecified); + assertTrue(anotherTask.compareTo(floatingTask) < 0); + } + +} diff --git a/src/test/java/seedu/agendum/model/ToDoListTest.java b/src/test/java/seedu/agendum/model/ToDoListTest.java new file mode 100644 index 000000000000..89c109b199ed --- /dev/null +++ b/src/test/java/seedu/agendum/model/ToDoListTest.java @@ -0,0 +1,61 @@ +package seedu.agendum.model; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.Name; +import seedu.agendum.model.task.Task; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +//@@author A0148095X +public class ToDoListTest { + + private Task alice, bob, charlie; + private ToDoList one, another; + + @Before + public void setUp() throws IllegalValueException{ + alice = new Task(new Name("meet alice")); + bob = new Task(new Name("meet bob")); + charlie = new Task(new Name("meet charlie")); + + one = new ToDoList(); + one.addTask(alice); + one.addTask(bob); + + another = new ToDoList(); + another.addTask(alice); + another.addTask(bob); + } + + + @Test + public void equals_symmetric_returnsTrue() { + assertTrue(one.equals(another) && another.equals(one)); + } + + @Test + public void hashCode_symmetric_returnsTrue() { + assertTrue(one.hashCode() == another.hashCode()); + } + + @Test + public void getEmptyToDoList() { + assertTrue(ToDoList.getEmptyToDoList().getTaskList().isEmpty()); + } + + @Test + public void removeTask_taskExists_removedSuccessfully() throws TaskNotFoundException { + one.removeTask(alice); + assertFalse(one.getTaskList().contains(alice)); + } + + @Test (expected = TaskNotFoundException.class) + public void removeTask_taskDoesNotExist_throwsTaskNotFoundException() throws TaskNotFoundException { + one.removeTask(charlie); + } +} diff --git a/src/test/java/seedu/agendum/model/UniqueTaskListTest.java b/src/test/java/seedu/agendum/model/UniqueTaskListTest.java new file mode 100644 index 000000000000..1dfc4b037fc8 --- /dev/null +++ b/src/test/java/seedu/agendum/model/UniqueTaskListTest.java @@ -0,0 +1,139 @@ +package seedu.agendum.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.Name; +import seedu.agendum.model.task.Task; +import seedu.agendum.model.task.UniqueTaskList; +import seedu.agendum.model.task.UniqueTaskList.DuplicateTaskException; +import seedu.agendum.model.task.UniqueTaskList.TaskNotFoundException; + +//@@author A0133367E +public class UniqueTaskListTest { + + private UniqueTaskList uniqueTaskList; + private ObservableList internalList; + + private Task originalTask; + private Task duplicateOfOriginalTask; + private Task newTask; + + @Before + public void setUp() throws IllegalValueException { + uniqueTaskList = new UniqueTaskList(); + + originalTask = new Task(new Name("task")); + duplicateOfOriginalTask = new Task(originalTask); + newTask = new Task(new Name("new task")); + + uniqueTaskList.add(originalTask); + + internalList = FXCollections.observableArrayList(); + internalList.add(originalTask); + } + + @Test + public void contains_taskInEmptyList_returnsFalse() { + uniqueTaskList = new UniqueTaskList(); + assertFalse(uniqueTaskList.contains(originalTask)); + } + + @Test + public void contains_taskWithDifferentState_returnsFalse() throws Exception { + assertFalse(uniqueTaskList.contains(newTask)); + } + + @Test + public void contains_taskWithSameState_returnsTrue() throws Exception { + assertTrue(uniqueTaskList.contains(duplicateOfOriginalTask)); + } + + @Test(expected = DuplicateTaskException.class) + public void add_duplicateTask_throwsException() throws Exception { + uniqueTaskList.add(duplicateOfOriginalTask); + } + + @Test + public void add_newTask_successful() throws Exception { + uniqueTaskList.add(newTask); + internalList.add(newTask); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = TaskNotFoundException.class) + public void remove_absentTask_throwsException() throws Exception { + uniqueTaskList.remove(newTask); + } + + @Test + public void remove_existingTask_successful() throws Exception { + uniqueTaskList.remove(duplicateOfOriginalTask); + internalList.clear(); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = DuplicateTaskException.class) + public void update_duplicateTask_throwsException() throws Exception { + uniqueTaskList.add(newTask); + uniqueTaskList.update(newTask, duplicateOfOriginalTask); + } + + @Test(expected = TaskNotFoundException.class) + public void update_absentTask_throwsException() throws Exception { + uniqueTaskList.update(newTask, newTask); + } + + @Test + public void update_existingTask_successful() throws Exception { + uniqueTaskList.update(originalTask, newTask); + internalList.set(0, newTask); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = DuplicateTaskException.class) + public void mark_resultInDuplicateTask_throwsException() throws Exception { + duplicateOfOriginalTask.markAsCompleted(); + uniqueTaskList.add(duplicateOfOriginalTask); + uniqueTaskList.mark(originalTask); + } + + @Test(expected = TaskNotFoundException.class) + public void mark_absentTask_throwsException() throws Exception { + uniqueTaskList.mark(newTask); + } + + @Test + public void mark_existingTask_successful() throws Exception { + uniqueTaskList.mark(originalTask); + internalList.get(0).markAsCompleted(); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } + + @Test(expected = DuplicateTaskException.class) + public void unmark_resultInDuplicateTask_throwsException() throws Exception { + // cannot unmark a task that have not been mark as completed + uniqueTaskList.unmark(originalTask); + } + + @Test(expected = TaskNotFoundException.class) + public void unmark_absentTask_throwsException() throws Exception { + uniqueTaskList.unmark(newTask); + } + + @Test + public void unmark_existingTask_successful() throws Exception { + uniqueTaskList = new UniqueTaskList(); + duplicateOfOriginalTask.markAsCompleted(); + uniqueTaskList.add(duplicateOfOriginalTask); + uniqueTaskList.unmark(duplicateOfOriginalTask); + assertEquals(uniqueTaskList.getInternalList(), internalList); + } +} \ No newline at end of file diff --git a/src/test/java/seedu/agendum/model/UnmodifiableObservableListTest.java b/src/test/java/seedu/agendum/model/UnmodifiableObservableListTest.java new file mode 100644 index 000000000000..fe698564cd5f --- /dev/null +++ b/src/test/java/seedu/agendum/model/UnmodifiableObservableListTest.java @@ -0,0 +1,230 @@ +package seedu.agendum.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static seedu.agendum.testutil.TestUtil.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.junit.Before; +import org.junit.Test; + +import javafx.collections.FXCollections; +import seedu.agendum.commons.core.UnmodifiableObservableList; + +public class UnmodifiableObservableListTest { + + private final int ITEM_ZERO = 10; + private final int ITEM_ONE = 11; + private final int ITEM_TWO = 12; + private final int ITEM_THREE = 13; + private final int ITEM_FOUR = 14; + + + private List backing; + private UnmodifiableObservableList list; + + @Before + public void setUp() { + backing = new ArrayList<>(); + backing.add(ITEM_ZERO); + backing.add(ITEM_ONE); + backing.add(ITEM_TWO); + backing.add(ITEM_THREE); + backing.add(ITEM_FOUR); + list = new UnmodifiableObservableList<>(FXCollections.observableList(backing)); + } + + @Test + public void transformationListGeneratorsCorrectBackingList() { + assertSame(list.sorted().getSource(), list); + assertSame(list.filtered(i -> true).getSource(), list); + } + + //@@author A0148095X + @Test + public void mutatingMethodsDisabled() { + + final Class ex = UnsupportedOperationException.class; + + assertThrows(ex, () -> list.add(3)); + assertThrows(ex, () -> list.add(1, 2)); + assertThrows(ex, () -> list.add(1, null)); + + assertThrows(ex, () -> list.addAll(2, 1)); + assertThrows(ex, () -> list.addAll(null, 1)); + assertThrows(ex, () -> list.addAll(backing)); + assertThrows(ex, () -> list.addAll(0, backing)); + + assertThrows(ex, () -> list.set(0, 2)); + assertThrows(ex, () -> list.set(1, null)); + + assertThrows(ex, () -> list.setAll(1, 2)); + assertThrows(ex, () -> list.setAll(1, null)); + assertThrows(ex, () -> list.setAll(backing)); + + assertThrows(ex, () -> list.remove(0, 1)); + assertThrows(ex, () -> list.remove(null)); + assertThrows(ex, () -> list.remove(0)); + + assertThrows(ex, () -> list.removeAll(backing)); + assertThrows(ex, () -> list.removeAll(1, 2)); + assertThrows(ex, () -> list.removeAll(null, 2)); + + assertThrows(ex, () -> list.retainAll(backing)); + assertThrows(ex, () -> list.retainAll(1, 2)); + assertThrows(ex, () -> list.retainAll(1, null)); + + assertThrows(ex, () -> list.replaceAll(i -> 1)); + + assertThrows(ex, () -> list.sort(Comparator.naturalOrder())); + assertThrows(ex, () -> list.sort(null)); + + assertThrows(ex, () -> list.clear()); + + final Iterator iter = list.iterator(); + iter.next(); + assertThrows(ex, iter::remove); + + final ListIterator liter = list.listIterator(); + liter.next(); + assertThrows(ex, liter::remove); + assertThrows(ex, () -> liter.add(5)); + assertThrows(ex, () -> liter.set(3)); + assertThrows(ex, () -> list.removeIf(i -> true)); + } + + @SuppressWarnings("unused") + @Test (expected = NullPointerException.class) + public void unmodifiableObservableList_nullList_nullPointerExceptionThrown() { + UnmodifiableObservableList nullList = new UnmodifiableObservableList<>(null); + } + + @Test + public void isEmpty_nonEmptyList_returnsFalse() { + assertFalse(list.isEmpty()); + } + + @Test + public void contains_existingItem_returnsTrue() { + assertTrue(list.contains(ITEM_TWO)); + } + + @Test + public void containsAll_sameList_returnsTrue() { + assertTrue(list.containsAll(backing)); + } + + @Test + public void containsAll_listWhichContainsOneExtraItem_returnsFalse() { + final List backingWithOneExtraItem = new ArrayList<>(backing); + backingWithOneExtraItem.add(15); + final UnmodifiableObservableList listWithOneExtraItem = new UnmodifiableObservableList<>(FXCollections.observableList(backingWithOneExtraItem)); + + assertFalse(list.containsAll(listWithOneExtraItem)); + } + + @Test + public void indexOf_validItem_returnsCorrectIndex() { + final int itemToFind = ITEM_ONE; + final int index = list.indexOf(itemToFind); + + assertEquals(index, 1); + } + + @Test + public void indexOf_invalidItem_returnsErrorIndex() { + final int itemToFind = 999; + final int index = list.indexOf(itemToFind); + + assertEquals(index, -1); + } + + @Test + public void lastIndexOf_validItem_returnsCorrectIndex() { + final int itemToFind = ITEM_ZERO; + final int itemToAdd = ITEM_ZERO; // add a duplicate so it become last object + + final List backingWithDuplicate = new ArrayList<>(backing); + backingWithDuplicate.add(itemToAdd); + final UnmodifiableObservableList listWithDuplicate = new UnmodifiableObservableList<>(FXCollections.observableList(backingWithDuplicate)); + + final int expectedIndex = listWithDuplicate.size()-1; + final int actualIndex = listWithDuplicate.lastIndexOf(itemToFind); + + assertEquals(expectedIndex, actualIndex); + } + + @Test + public void lastIndexOf_invalidItem_returnsErrorIndex() { + final int itemToFind = 888; + final int index = list.lastIndexOf(itemToFind); + + assertEquals(index, -1); + } + + @Test + public void subList_sameItems_returnsTrue() { + final int startIndex = 1; + final int endIndex = 3; + List subListOfBacking = backing.subList(startIndex, endIndex); + List subListOfList = list.subList(startIndex, endIndex); + + assertTrue(subListOfBacking.equals(subListOfList)); + } + + @Test + public void toArray_sameItems_returnsTrue() { + final Integer[] arrayWithSameItems = new Integer[]{ITEM_ZERO, ITEM_ONE, ITEM_TWO, ITEM_THREE, ITEM_FOUR}; + final Object[] convertedToObjectArray = list.toArray(); + final Integer[] convertedToIntegerArray = list.toArray(new Integer[0]); + + assertTrue(Arrays.deepEquals(arrayWithSameItems, convertedToObjectArray)); + assertTrue(Arrays.equals(arrayWithSameItems, convertedToIntegerArray)); + } + + @Test + public void equals_symmetricList_returnsTrue() { + final UnmodifiableObservableList one = new UnmodifiableObservableList<>(FXCollections.observableList(backing)); + final UnmodifiableObservableList another = new UnmodifiableObservableList<>(FXCollections.observableList(backing)); + + assertTrue(one.equals(another) && another.equals(one)); + assertTrue(one.hashCode() == another.hashCode()); + } + + @Test + public void listIterator_iterateWholeList_listMatches() { + final ListIterator liter = list.listIterator(); + int currentItem; + int index; + + // cursor position 0 -> 1 (index 0) + assertTrue(liter.hasNext()); + + index = liter.nextIndex(); + assertEquals(index, 0); + + currentItem = liter.next(); + assertEquals(currentItem, ITEM_ZERO); + + // move cursor position 1 -> 2 -> 3 + liter.next(); + liter.next(); + + // cursor position 3 -> 2 (index 2) + assertTrue(liter.hasPrevious()); + + index = liter.previousIndex(); + assertEquals(index, 2); + + currentItem = liter.previous(); + assertEquals(currentItem, ITEM_TWO); + } +} diff --git a/src/test/java/seedu/agendum/model/UserPrefsTest.java b/src/test/java/seedu/agendum/model/UserPrefsTest.java new file mode 100644 index 000000000000..f03f0ad6385a --- /dev/null +++ b/src/test/java/seedu/agendum/model/UserPrefsTest.java @@ -0,0 +1,55 @@ +package seedu.agendum.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.commons.core.GuiSettings; + +//@@author A0148095X +public class UserPrefsTest { + + private UserPrefs one, another; + + @Before + public void setUp() { + one = new UserPrefs(); + another = new UserPrefs(); + } + + @Test + public void equals_differentObject_returnsFalse() { + assertFalse(one.equals(new Object())); + } + + @Test + public void equals_symmetric_returnsTrue() { + // equals to itself and object with same parameters + assertTrue(one.equals(one)); + assertTrue(one.equals(another)); + } + + @Test + public void hashcode_symmetric_returnsTrue() { + assertEquals(one.hashCode(), another.hashCode()); + } + + @Test + public void setGuiSettings_validInputs_successful() { + final double expectedWidth = 222; + final double expectedHeight = 333; + final int expectedX = 444; + final int expectedY = 555; + final GuiSettings expectedGuiSettings = new GuiSettings(expectedWidth, expectedHeight, expectedX, expectedY); + + final UserPrefs userPrefs = new UserPrefs(); + userPrefs.setGuiSettings(expectedWidth, expectedHeight, expectedX, expectedY); + GuiSettings actualGuiSettings = userPrefs.getGuiSettings(); + + assertEquals(actualGuiSettings, expectedGuiSettings); + } + +} diff --git a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java b/src/test/java/seedu/agendum/storage/JsonUserPrefsStorageTest.java similarity index 70% rename from src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java rename to src/test/java/seedu/agendum/storage/JsonUserPrefsStorageTest.java index 4e87203611be..4f8f0fbb7518 100644 --- a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java +++ b/src/test/java/seedu/agendum/storage/JsonUserPrefsStorageTest.java @@ -1,13 +1,12 @@ -package seedu.address.storage; +package seedu.agendum.storage; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.FileUtil; -import seedu.address.model.UserPrefs; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.model.UserPrefs; import java.io.File; import java.io.IOException; @@ -20,15 +19,11 @@ public class JsonUserPrefsStorageTest { private static String TEST_DATA_FOLDER = FileUtil.getPath("./src/test/data/JsonUserPrefsStorageTest/"); - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Rule public TemporaryFolder testFolder = new TemporaryFolder(); - @Test - public void readUserPrefs_nullFilePath_assertionFailure() throws DataConversionException { - thrown.expect(AssertionError.class); + @Test(expected = AssertionError.class) + public void readUserPrefsNullFilePathSssertionFailure() throws DataConversionException { readUserPrefs(null); } @@ -38,14 +33,13 @@ private Optional readUserPrefs(String userPrefsFileInTestDataFolder) } @Test - public void readUserPrefs_missingFile_emptyResult() throws DataConversionException { + public void readUserPrefsMissingFileEmptyResult() throws DataConversionException { assertFalse(readUserPrefs("NonExistentFile.json").isPresent()); } - @Test - public void readUserPrefs_notJasonFormat_exceptionThrown() throws DataConversionException { + @Test(expected = DataConversionException.class) + public void readUserPrefsNotJasonFormatExceptionThrown() throws DataConversionException { - thrown.expect(DataConversionException.class); readUserPrefs("NotJsonFormatUserPrefs.json"); /* IMPORTANT: Any code below an exception-throwing line (like the one above) will be ignored. @@ -60,7 +54,7 @@ private String addToTestDataPathIfNotNull(String userPrefsFileInTestDataFolder) } @Test - public void readUserPrefs_fileInOrder_successfullyRead() throws DataConversionException { + public void readUserPrefsFileInOrderSuccessfullyRead() throws DataConversionException { UserPrefs expected = new UserPrefs(); expected.setGuiSettings(1000, 500, 300, 100); UserPrefs actual = readUserPrefs("TypicalUserPref.json").get(); @@ -68,13 +62,13 @@ public void readUserPrefs_fileInOrder_successfullyRead() throws DataConversionEx } @Test - public void readUserPrefs_valuesMissingFromFile_defaultValuesUsed() throws DataConversionException { + public void readUserPrefsValuesMissingFromFileDefaultValuesUsed() throws DataConversionException { UserPrefs actual = readUserPrefs("EmptyUserPrefs.json").get(); assertEquals(new UserPrefs(), actual); } @Test - public void readUserPrefs_extraValuesInFile_extraValuesIgnored() throws DataConversionException { + public void readUserPrefsExtraValuesInFileExtraValuesIgnored() throws DataConversionException { UserPrefs expected = new UserPrefs(); expected.setGuiSettings(1000, 500, 300, 100); UserPrefs actual = readUserPrefs("ExtraValuesUserPref.json").get(); @@ -82,15 +76,13 @@ public void readUserPrefs_extraValuesInFile_extraValuesIgnored() throws DataConv assertEquals(expected, actual); } - @Test - public void savePrefs_nullPrefs_assertionFailure() throws IOException { - thrown.expect(AssertionError.class); + @Test(expected = AssertionError.class) + public void savePrefsNullPrefsAssertionFailure() throws IOException { saveUserPrefs(null, "SomeFile.json"); } - @Test - public void saveUserPrefs_nullFilePath_assertionFailure() throws IOException { - thrown.expect(AssertionError.class); + @Test(expected = AssertionError.class) + public void saveUserPrefsNullFilePathAssertionFailure() throws IOException { saveUserPrefs(new UserPrefs(), null); } @@ -99,7 +91,7 @@ private void saveUserPrefs(UserPrefs userPrefs, String prefsFileInTestDataFolder } @Test - public void saveUserPrefs_allInOrder_success() throws DataConversionException, IOException { + public void saveUserPrefsAllInOrderSuccess() throws DataConversionException, IOException { UserPrefs original = new UserPrefs(); original.setGuiSettings(1200, 200, 0, 2); diff --git a/src/test/java/seedu/agendum/storage/StorageManagerTest.java b/src/test/java/seedu/agendum/storage/StorageManagerTest.java new file mode 100644 index 000000000000..728e2e21cbec --- /dev/null +++ b/src/test/java/seedu/agendum/storage/StorageManagerTest.java @@ -0,0 +1,173 @@ +package seedu.agendum.storage; + + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.Hashtable; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import seedu.agendum.commons.events.model.ChangeSaveLocationEvent; +import seedu.agendum.commons.events.model.LoadDataRequestEvent; +import seedu.agendum.commons.events.model.ToDoListChangedEvent; +import seedu.agendum.commons.events.storage.DataLoadingExceptionEvent; +import seedu.agendum.commons.events.storage.DataSavingExceptionEvent; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.exceptions.FileDeletionException; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.model.ReadOnlyToDoList; +import seedu.agendum.model.ToDoList; +import seedu.agendum.model.UserPrefs; +import seedu.agendum.testutil.EventsCollector; +import seedu.agendum.testutil.TestUtil; +import seedu.agendum.testutil.TypicalTestTasks; + +public class StorageManagerTest { + + private StorageManager storageManager; + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + @Before + public void setUp() { + storageManager = new StorageManager(getTempFilePath("ab"), getTempFilePath("command"), + getTempFilePath("prefs"), TestUtil.createTempConfig()); + } + + + private String getTempFilePath(String fileName) { + return testFolder.getRoot().getPath() + fileName; + } + + + /* + * Note: This is an integration test that verifies the StorageManager is properly wired to the + * {@link JsonUserPrefsStorage} class. + * More extensive testing of UserPref saving/reading is done in {@link JsonUserPrefsStorageTest} class. + */ + + @Test + public void prefsReadSave() throws Exception { + UserPrefs original = new UserPrefs(); + original.setGuiSettings(300, 600, 4, 6); + storageManager.saveUserPrefs(original); + UserPrefs retrieved = storageManager.readUserPrefs().get(); + assertEquals(original, retrieved); + } + + /** + * Verifies that StorageManager is properly wired to {@link JsonAliasTableStorage} class + */ + @Test + public void aliasTableReadSave() throws Exception { + Hashtable testingTable = new Hashtable(); + testingTable.put("a", "add"); + testingTable.put("d", "delete"); + storageManager.saveAliasTable(testingTable); + Hashtable retrieved = storageManager.readAliasTable().get(); + assertEquals(testingTable, retrieved); + } + + @Test + public void toDoListReadSave() throws Exception { + ToDoList original = new TypicalTestTasks().getTypicalToDoList(); + storageManager.saveToDoList(original); + ReadOnlyToDoList retrieved = storageManager.readToDoList().get(); + assertEquals(original, new ToDoList(retrieved)); + //More extensive testing of ToDoList saving/reading is done in XmlToDoListStorageTest + } + + @Test + public void getToDoListFilePath(){ + assertNotNull(storageManager.getToDoListFilePath()); + } + + @Test + public void handleToDoListChangedEventExceptionThrownEventRaised() throws IOException { + //Create a StorageManager while injecting a stub that throws an exception when the save method is called + Storage storage = new StorageManager(new XmlToDoListStorageExceptionThrowingStub("dummy"), + new JsonAliasTableStorage("dummy"), new JsonUserPrefsStorage("dummy"), TestUtil.createTempConfig()); + EventsCollector eventCollector = new EventsCollector(); + storage.handleToDoListChangedEvent(new ToDoListChangedEvent(new ToDoList())); + assertTrue(eventCollector.get(0) instanceof DataSavingExceptionEvent); + } + + //@@author A0148095X + @Test + public void handleSaveLocationChangedEvent_validFilePath_success() { + String validPath = "data/test.xml"; + storageManager.handleChangeSaveLocationEvent(new ChangeSaveLocationEvent(validPath)); + assertEquals(storageManager.getToDoListFilePath(), validPath); + } + + @Test + public void handleLoadDataRequestEvent_validPathToFileInvalidFile_throwsException() throws IOException, FileDeletionException { + EventsCollector eventCollector = new EventsCollector(); + String validPath = "data/testLoad.xml"; + assert !FileUtil.isFileExists(validPath); + + // File does not exist + storageManager.handleLoadDataRequestEvent(new LoadDataRequestEvent(validPath)); + DataLoadingExceptionEvent dlee = (DataLoadingExceptionEvent)eventCollector.get(0); + assertTrue(dlee.exception instanceof NoSuchElementException); + + // File in wrong format + FileUtil.createFile(new File(validPath)); + storageManager.handleLoadDataRequestEvent(new LoadDataRequestEvent(validPath)); + dlee = (DataLoadingExceptionEvent)eventCollector.get(1); + assertTrue(dlee.exception instanceof DataConversionException); + FileUtil.deleteFile(validPath); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_nullPath_fail() { + // null + storageManager.setToDoListFilePath(null); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_pathEmpty_fail() { + // empty string + storageManager.setToDoListFilePath(""); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_pathInvalid_fail() { + // invalid file path + storageManager.setToDoListFilePath("1:/.xml"); + } + + public void setToDoListFilePath_pathValid_success() { + // valid file path + String validPath = "test/test.xml"; + storageManager.setToDoListFilePath(validPath); + assertEquals(validPath, storageManager.getToDoListFilePath()); + } + //@@author + + /** + * A Stub class to throw an exception when the save method is called + */ + class XmlToDoListStorageExceptionThrowingStub extends XmlToDoListStorage{ + + public XmlToDoListStorageExceptionThrowingStub(String filePath) { + super(filePath); + } + + @Override + public void saveToDoList(ReadOnlyToDoList toDoList, String filePath) throws IOException { + throw new IOException("dummy exception"); + } + } + + +} diff --git a/src/test/java/seedu/agendum/storage/XmlAdaptedTaskTest.java b/src/test/java/seedu/agendum/storage/XmlAdaptedTaskTest.java new file mode 100644 index 000000000000..6e2ebd442e6a --- /dev/null +++ b/src/test/java/seedu/agendum/storage/XmlAdaptedTaskTest.java @@ -0,0 +1,80 @@ +package seedu.agendum.storage; + +import static org.junit.Assert.assertTrue; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.Name; +import seedu.agendum.model.task.Task; + +//@@author A0148095X +public class XmlAdaptedTaskTest { + + private Optional optionalStartDateTime; + private Optional optionalEndDateTime; + + private XmlAdaptedTask xmlAdaptedTaskAllFields; + private XmlAdaptedTask xmlAdaptedTaskUncompleted; + private XmlAdaptedTask xmlAdaptedTaskNoStartDateTime; + private XmlAdaptedTask xmlAdaptedTaskNoEndDateTime; + + @Before + public void setUp() throws IllegalValueException { + LocalDate date = LocalDate.now(); + + LocalTime startTime = LocalTime.of(12, 0); // 12pm + LocalDateTime startDateTime = LocalDateTime.of(date, startTime); + optionalStartDateTime = Optional.of(startDateTime); + + LocalTime endTime = LocalTime.of(13, 0); // 1pm + LocalDateTime endDateTime = LocalDateTime.of(date, endTime); + optionalEndDateTime = Optional.of(endDateTime); + + Task taskWithAllFields = new Task(new Name("taskWithStartAndEndDateTimeCompleted"), optionalStartDateTime, optionalEndDateTime); + taskWithAllFields.markAsCompleted(); + xmlAdaptedTaskAllFields = new XmlAdaptedTask(taskWithAllFields); + + Task taskMarkedAsUncompleted = new Task(new Name("taskWithStartAndEndDateTimeUncompleted"), optionalStartDateTime, optionalEndDateTime); + xmlAdaptedTaskUncompleted = new XmlAdaptedTask(taskMarkedAsUncompleted); + + Task taskWithNoStartDateTime = new Task(new Name("taskWithNoStartDateTime"), Optional.ofNullable(null), optionalEndDateTime); + taskWithNoStartDateTime.markAsCompleted(); + xmlAdaptedTaskNoStartDateTime = new XmlAdaptedTask(taskWithNoStartDateTime); + + Task taskWithNoEndDateTime = new Task(new Name("taskWithNoEndDateTime"), optionalStartDateTime, Optional.ofNullable(null)); + taskWithNoEndDateTime.markAsCompleted(); + xmlAdaptedTaskNoEndDateTime = new XmlAdaptedTask(taskWithNoEndDateTime); + } + + public void assertTaskEqual(Task task, Optional startDateTime, Optional endDateTime, boolean isCompleted) { + assertTrue(task.getStartDateTime().equals(startDateTime)); + assertTrue(task.getEndDateTime().equals(endDateTime)); + assertTrue(task.isCompleted() == isCompleted); + } + + @Test + public void toModelType() throws IllegalValueException { + // Task with start date time, end date time, completed + Task taskWithAllFields = xmlAdaptedTaskAllFields.toModelType(); + assertTaskEqual(taskWithAllFields, optionalStartDateTime, optionalEndDateTime, true); + + // Task with start date time, end date time, not completed + Task taskMarkedAsUncompleted = xmlAdaptedTaskUncompleted.toModelType(); + assertTaskEqual(taskMarkedAsUncompleted, optionalStartDateTime, optionalEndDateTime, false); + + // Task with no start date time, is completed + Task taskWithNoStartDateTime = xmlAdaptedTaskNoStartDateTime.toModelType(); + assertTaskEqual(taskWithNoStartDateTime, Optional.empty(), optionalEndDateTime, true); + + // Task with no end date time, is completed + Task taskWithNoEndDateTime = xmlAdaptedTaskNoEndDateTime.toModelType(); + assertTaskEqual(taskWithNoEndDateTime, optionalStartDateTime, Optional.empty(), true); + } +} diff --git a/src/test/java/seedu/agendum/storage/XmlToDoListStorageTest.java b/src/test/java/seedu/agendum/storage/XmlToDoListStorageTest.java new file mode 100644 index 000000000000..ea431177a67d --- /dev/null +++ b/src/test/java/seedu/agendum/storage/XmlToDoListStorageTest.java @@ -0,0 +1,133 @@ +package seedu.agendum.storage; + + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import seedu.agendum.commons.exceptions.DataConversionException; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.model.ReadOnlyToDoList; +import seedu.agendum.model.ToDoList; +import seedu.agendum.model.task.Task; +import seedu.agendum.testutil.TypicalTestTasks; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class XmlToDoListStorageTest { + private static String TEST_DATA_FOLDER = FileUtil.getPath("./src/test/data/XmlToDoListStorageTest/"); + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + @Test(expected = AssertionError.class) + public void readToDoListNullFilePathAssertionFailure() throws Exception { + readToDoList(null); + } + + private java.util.Optional readToDoList(String filePath) throws Exception { + return new XmlToDoListStorage(filePath).readToDoList(addToTestDataPathIfNotNull(filePath)); + } + + private String addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { + return prefsFileInTestDataFolder != null + ? TEST_DATA_FOLDER + prefsFileInTestDataFolder + : null; + } + + @Test + public void readMissingFileEmptyResult() throws Exception { + assertFalse(readToDoList("NonExistentFile.xml").isPresent()); + } + + @Test(expected = DataConversionException.class) + public void readNotXmlFormatExceptionThrown() throws Exception { + + readToDoList("NotXmlFormatToDoList.xml"); + + /* IMPORTANT: Any code below an exception-throwing line (like the one above) will be ignored. + * That means you should not have more than one exception test in one method + */ + } + + @Test + public void readAndSaveToDoListAllInOrderSuccess() throws Exception { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + TypicalTestTasks td = new TypicalTestTasks(); + ToDoList original = td.getTypicalToDoList(); + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + //Save in new file and read back + xmlToDoListStorage.saveToDoList(original, filePath); + ReadOnlyToDoList readBack = xmlToDoListStorage.readToDoList(filePath).get(); + assertEquals(original, new ToDoList(readBack)); + + //Modify data, overwrite exiting file, and read back + original.addTask(new Task(TypicalTestTasks.HOON)); + original.removeTask(new Task(TypicalTestTasks.ALICE)); + xmlToDoListStorage.saveToDoList(original, filePath); + readBack = xmlToDoListStorage.readToDoList(filePath).get(); + assertEquals(original, new ToDoList(readBack)); + + //Save and read without specifying file path + original.addTask(new Task(TypicalTestTasks.IDA)); + xmlToDoListStorage.saveToDoList(original); //file path not specified + readBack = xmlToDoListStorage.readToDoList().get(); //file path not specified + assertEquals(original, new ToDoList(readBack)); + + } + + @Test(expected = AssertionError.class) + public void saveToDoListNullToDoListAssertionFailure() throws IOException { + saveToDoList(null, "SomeFile.xml"); + } + + private void saveToDoList(ReadOnlyToDoList toDoList, String filePath) throws IOException { + new XmlToDoListStorage(filePath).saveToDoList(toDoList, addToTestDataPathIfNotNull(filePath)); + } + + @Test(expected = AssertionError.class) + public void saveToDoListNullFilePathAssertionFailure() throws IOException { + saveToDoList(new ToDoList(), null); + } + + //@@author A0148095X + @Test(expected = AssertionError.class) + public void setToDoListFilePath_nullPath_throwsAssertionError() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + xmlToDoListStorage.setToDoListFilePath(null); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_emptyPath_throwsAssertionError() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + // empty string + xmlToDoListStorage.setToDoListFilePath(""); + } + + @Test(expected = AssertionError.class) + public void setToDoListFilePath_invalidPath_throwsAssertionError() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + // invalid file path + xmlToDoListStorage.setToDoListFilePath("1:/.xml"); + } + + public void setToDoListFilePath_validPath_success() { + String filePath = testFolder.getRoot().getPath() + "TempToDoList.xml"; + XmlToDoListStorage xmlToDoListStorage = new XmlToDoListStorage(filePath); + + // valid file path + String validPath = "test/test.xml"; + xmlToDoListStorage.setToDoListFilePath(validPath); + assertEquals(validPath, xmlToDoListStorage.getToDoListFilePath()); + } + +} diff --git a/src/test/java/seedu/agendum/sync/SyncManagerTests.java b/src/test/java/seedu/agendum/sync/SyncManagerTests.java new file mode 100644 index 000000000000..c1a08e49cbd5 --- /dev/null +++ b/src/test/java/seedu/agendum/sync/SyncManagerTests.java @@ -0,0 +1,137 @@ +package seedu.agendum.sync; + +import org.junit.Before; +import org.junit.Test; +import seedu.agendum.model.task.Task; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; + +import static org.mockito.Mockito.*; + +// @@author A0003878Y +public class SyncManagerTests { + private SyncManager syncManager; + private SyncProvider mockSyncProvider; + + @Before + public void setUp() { + mockSyncProvider = mock(SyncProvider.class); + syncManager = new SyncManager(mockSyncProvider); + } + + @Test + public void syncManager_setStatusRunning_expectRunning() { + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + assertEquals(syncManager.getSyncStatus(),Sync.SyncStatus.RUNNING); + } + + @Test + public void syncManager_setStatusNotRunning_expectNotRunning() { + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + assertEquals(syncManager.getSyncStatus(),Sync.SyncStatus.NOTRUNNING); + } + + @Test + public void syncManager_startSyncing_expectSyncProviderStart() { + syncManager.startSyncing(); + verify(mockSyncProvider).start(); + } + + @Test + public void syncManager_stopSyncing_expectSyncProviderStop() { + syncManager.stopSyncing(); + verify(mockSyncProvider).stop(); + } + + @Test + public void syncManager_addEventWithStartAndEndTime_expectSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional fakeTime = Optional.of(LocalDateTime.now()); + + when(mockTask.getStartDateTime()).thenReturn(fakeTime); + when(mockTask.getEndDateTime()).thenReturn(fakeTime); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithStartTime_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional fakeTime = Optional.of(LocalDateTime.now()); + Optional empty = Optional.empty(); + + when(mockTask.getStartDateTime()).thenReturn(empty); + when(mockTask.getEndDateTime()).thenReturn(fakeTime); + + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithEndTime_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional fakeTime = Optional.of(LocalDateTime.now()); + Optional empty = Optional.empty(); + + when(mockTask.getStartDateTime()).thenReturn(fakeTime); + when(mockTask.getEndDateTime()).thenReturn(empty); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithNoTime_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + Optional empty = Optional.empty(); + + when(mockTask.getStartDateTime()).thenReturn(empty); + when(mockTask.getEndDateTime()).thenReturn(empty); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_addEventWithSyncManagerNotRunning_expectNoSyncProviderAdd() { + Task mockTask = mock(Task.class); + + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + syncManager.addNewEvent(mockTask); + + verify(mockSyncProvider, never()).addNewEvent(mockTask); + } + + @Test + public void syncManager_deleteEventWithSyncManagerRunning_expectSyncProviderDelete() { + Task mockTask = mock(Task.class); + + syncManager.setSyncStatus(Sync.SyncStatus.RUNNING); + syncManager.deleteEvent(mockTask); + + verify(mockSyncProvider).deleteEvent(mockTask); + } + + @Test + public void syncManager_deleteEventWithSyncManagerNotRunning_expectNoSyncProviderDelete() { + Task mockTask = mock(Task.class); + + syncManager.setSyncStatus(Sync.SyncStatus.NOTRUNNING); + syncManager.deleteEvent(mockTask); + + verify(mockSyncProvider, never()).deleteEvent(mockTask); + } +} diff --git a/src/test/java/seedu/agendum/sync/SyncProviderGoogleTests.java b/src/test/java/seedu/agendum/sync/SyncProviderGoogleTests.java new file mode 100644 index 000000000000..08e0a7a62c06 --- /dev/null +++ b/src/test/java/seedu/agendum/sync/SyncProviderGoogleTests.java @@ -0,0 +1,132 @@ +package seedu.agendum.sync; + +import org.junit.*; +import org.junit.runners.MethodSorters; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.Name; +import seedu.agendum.model.task.Task; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +import static junit.framework.TestCase.assertFalse; +import static org.mockito.Mockito.*; +import static seedu.agendum.commons.core.Config.DEFAULT_DATA_DIR; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +// @@author A0003878Y +public class SyncProviderGoogleTests { + private static final File DATA_STORE_CREDENTIAL = new File(DEFAULT_DATA_DIR + "StoredCredential"); + + private static final List DATA_STORE_TEST_CREDENTIALS = Arrays.asList( + new File("cal/StoredCredential_1"), + new File("cal/StoredCredential_2"), + new File("cal/StoredCredential_3") + ); + + private static final SyncProviderGoogle syncProviderGoogle = spy(new SyncProviderGoogle()); + private static final SyncManager mockSyncManager = mock(SyncManager.class); + private static final Task mockTask = mock(Task.class); + + @BeforeClass + public static void setUp() { + copyTestCredentials(); + + try { + Optional fakeTime = Optional.of(LocalDateTime.now()); + Name fakeName = new Name("AGENDUMTESTENGINE"); + int minId = 99999; + int maxId = 9999999; + Random r = new Random(); + + when(mockTask.getStartDateTime()).thenReturn(fakeTime); + when(mockTask.getEndDateTime()).thenReturn(fakeTime); + when(mockTask.getName()).thenReturn(fakeName); + when(mockTask.syncCode()).thenReturn(r.nextInt((maxId - minId) + 1) + minId); + } catch (IllegalValueException e) { + e.printStackTrace(); + } + + syncProviderGoogle.setManager(mockSyncManager); + syncProviderGoogle.start(); + } + + @AfterClass + public static void tearDown() { + deleteCredential(); + } + + public static void copyTestCredentials() { + try { + deleteCredential(); + Files.copy(getRandomCredential().toPath(), DATA_STORE_CREDENTIAL.toPath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void deleteCredential() { + DATA_STORE_CREDENTIAL.delete(); + } + + private static File getRandomCredential() { + int r = new Random().nextInt(DATA_STORE_TEST_CREDENTIALS.size()); + return DATA_STORE_TEST_CREDENTIALS.get(r); + } + + @Test + public void syncProviderGoogle_start_createCalendar() { + reset(syncProviderGoogle); + syncProviderGoogle.deleteAgendumCalendar(); + syncProviderGoogle.start(); + + // Verify if Sync Manager's status was changed + verify(mockSyncManager, atLeastOnce()).setSyncStatus(Sync.SyncStatus.RUNNING); + } + + @Test + public void syncProviderGoogle_startIfNeeded_credentialsFound() { + reset(syncProviderGoogle); + syncProviderGoogle.startIfNeeded(); + + // Verify Sync Provider did start + verify(syncProviderGoogle).start(); + } + + @Test + public void syncProviderGoogle_startIfNeeded_credentialsNotFound() { + reset(syncProviderGoogle); + deleteCredential(); + syncProviderGoogle.startIfNeeded(); + + // Verify Sync Provider should not start + verify(syncProviderGoogle, never()).start(); + } + + @Test + public void syncProviderGoogle_stop_successful() { + reset(mockSyncManager); + syncProviderGoogle.stop(); + + // Verify sync status changed + verify(mockSyncManager).setSyncStatus(Sync.SyncStatus.NOTRUNNING); + assertFalse(DATA_STORE_CREDENTIAL.exists()); + } + + @Test + public void syncProviderGoogle_addEvent_successful() { + syncProviderGoogle.addNewEvent(mockTask); + } + + @Test + public void syncProviderGoogle_deleteEvent_successful() { + syncProviderGoogle.deleteEvent(mockTask); + } + +} diff --git a/src/test/java/seedu/address/testutil/EventsCollector.java b/src/test/java/seedu/agendum/testutil/EventsCollector.java similarity index 79% rename from src/test/java/seedu/address/testutil/EventsCollector.java rename to src/test/java/seedu/agendum/testutil/EventsCollector.java index c44d6ca6f95a..85683a62a7ad 100644 --- a/src/test/java/seedu/address/testutil/EventsCollector.java +++ b/src/test/java/seedu/agendum/testutil/EventsCollector.java @@ -1,8 +1,8 @@ -package seedu.address.testutil; +package seedu.agendum.testutil; import com.google.common.eventbus.Subscribe; -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.events.BaseEvent; +import seedu.agendum.commons.core.EventsCenter; +import seedu.agendum.commons.events.BaseEvent; import java.util.ArrayList; import java.util.List; @@ -11,7 +11,7 @@ * A class that collects events raised by other classes. */ public class EventsCollector{ - List events = new ArrayList(); + private List events = new ArrayList<>(); public EventsCollector(){ EventsCenter.getInstance().registerHandler(this); diff --git a/src/test/java/seedu/address/testutil/SerializableTestClass.java b/src/test/java/seedu/agendum/testutil/SerializableTestClass.java similarity index 96% rename from src/test/java/seedu/address/testutil/SerializableTestClass.java rename to src/test/java/seedu/agendum/testutil/SerializableTestClass.java index ef58ef857179..15b70d77d1cc 100644 --- a/src/test/java/seedu/address/testutil/SerializableTestClass.java +++ b/src/test/java/seedu/agendum/testutil/SerializableTestClass.java @@ -1,4 +1,4 @@ -package seedu.address.testutil; +package seedu.agendum.testutil; import java.time.LocalDateTime; import java.util.ArrayList; @@ -27,8 +27,6 @@ public class SerializableTestClass { private List listOfLocalDateTimes; private HashMap mapOfIntegerToString; - public SerializableTestClass() {} - public static String getNameTestValue() { return NAME_TEST_VALUE; } diff --git a/src/test/java/seedu/agendum/testutil/TaskBuilder.java b/src/test/java/seedu/agendum/testutil/TaskBuilder.java new file mode 100644 index 000000000000..550c43291e21 --- /dev/null +++ b/src/test/java/seedu/agendum/testutil/TaskBuilder.java @@ -0,0 +1,62 @@ +package seedu.agendum.testutil; + +import java.time.LocalDateTime; +import java.util.Optional; + +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.task.*; + +/** + * + */ +public class TaskBuilder { + + private TestTask task; + private LocalDateTime fixedTime = LocalDateTime.of(2016, 10, 10, 10, 10); + + public TaskBuilder() { + this.task = new TestTask(); + } + + /** + * Copy constructor + */ + public TaskBuilder(TestTask copy) { + this.task = new TestTask(copy); + } + + public TaskBuilder withName(String name) throws IllegalValueException { + this.task.setName(new Name(name)); + this.task.setLastUpdatedTime(fixedTime); + return this; + } + + public TaskBuilder withCompletedStatus() { + this.task.markAsCompleted(); + this.task.setLastUpdatedTime(fixedTime); + return this; + } + + public TaskBuilder withUncompletedStatus() { + this.task.markAsUncompleted(); + this.task.setLastUpdatedTime(fixedTime); + return this; + } + + public TaskBuilder withStartTime(LocalDateTime startTime) { + this.task.setStartDateTime(Optional.ofNullable(startTime)); + this.task.setLastUpdatedTime(fixedTime); + return this; + } + + public TaskBuilder withEndTime(LocalDateTime endTime) { + this.task.setEndDateTime(Optional.ofNullable(endTime)); + this.task.setLastUpdatedTime(fixedTime); + return this; + } + + public TestTask build() { + return this.task; + } + +} diff --git a/src/test/java/seedu/agendum/testutil/TestTask.java b/src/test/java/seedu/agendum/testutil/TestTask.java new file mode 100644 index 000000000000..65cb31df7896 --- /dev/null +++ b/src/test/java/seedu/agendum/testutil/TestTask.java @@ -0,0 +1,201 @@ +package seedu.agendum.testutil; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import seedu.agendum.model.task.*; + +/** + * A mutable task object. For testing only. + */ +public class TestTask implements ReadOnlyTask, Comparable { + + private static final int UPCOMING_DAYS_THRESHOLD = 7; + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("MM/dd HH:mm"); + private static final String DEADLINE_WORD = " by "; + private static final String EVENT_START_WORD = " from "; + private static final String EVENT_END_WORD = " to "; + + private Name name; + private boolean isCompleted; + private LocalDateTime startDateTime; + private LocalDateTime endDateTime; + private LocalDateTime lastUpdatedTime; + + public TestTask() { + this.isCompleted = false; + this.startDateTime = null; + this.endDateTime = null; + setLastUpdatedTimeToNow(); + } + + /** + * Copy constructor. + */ + public TestTask(TestTask other) { + this.name = other.name; + this.isCompleted = other.isCompleted; + this.startDateTime = other.startDateTime; + this.endDateTime = other.endDateTime; + this.lastUpdatedTime = other.getLastUpdatedTime(); + } + + public void setName(Name name) { + this.name = name; + setLastUpdatedTimeToNow(); + } + + public void markAsCompleted() { + this.isCompleted = true; + setLastUpdatedTimeToNow(); + } + + public void markAsUncompleted() { + this.isCompleted = false; + setLastUpdatedTimeToNow(); + } + + public void setStartDateTime(Optional startDateTime) { + this.startDateTime = startDateTime.orElse(null); + setLastUpdatedTimeToNow(); + } + + public void setEndDateTime(Optional endDateTime) { + this.endDateTime = endDateTime.orElse(null); + setLastUpdatedTimeToNow(); + } + + public void setLastUpdatedTimeToNow() { + this.lastUpdatedTime = LocalDateTime.now().withNano(0); + } + + public void setLastUpdatedTime(LocalDateTime updatedTime) { + this.lastUpdatedTime = updatedTime; + } + + @Override + public Name getName() { + return name; + } + + @Override + public boolean isCompleted() { + return isCompleted; + } + + public boolean isUpcoming() { + return !isCompleted() && hasTime() && !isOverdue() && + getTaskTime().isBefore(LocalDateTime.now().plusDays(UPCOMING_DAYS_THRESHOLD)); + } + + @Override + public boolean isOverdue() { + return !isCompleted() && hasTime() && getTaskTime().isBefore(LocalDateTime.now()); + } + + @Override + public boolean hasTime() { + return (getStartDateTime().isPresent() || getEndDateTime().isPresent()); + } + + @Override + public boolean isEvent() { + return getStartDateTime().isPresent(); + } + + @Override + public boolean hasDeadline() { + return !getStartDateTime().isPresent() && getEndDateTime().isPresent(); + } + + @Override + public Optional getStartDateTime() { + return Optional.ofNullable(startDateTime); + } + + @Override + public Optional getEndDateTime() { + return Optional.ofNullable(endDateTime); + } + + @Override + public LocalDateTime getLastUpdatedTime() { + return lastUpdatedTime; + } + + /** + * Pre-condition: Task has a start or end time + * Return the (earlier) time associated with the task + */ + private LocalDateTime getTaskTime() { + assert hasTime(); + return getStartDateTime().orElse(getEndDateTime().get()); + } + + @Override + public String toString() { + return getAsText(); + } + + public String getAddCommand() { + StringBuilder command = new StringBuilder(); + command.append("add " + this.getName().fullName + " "); + if (isEvent()) { + command.append(EVENT_START_WORD); + command.append(startDateTime.format(FORMATTER)); + command.append(EVENT_END_WORD); + command.append(endDateTime.format(FORMATTER)); + } else if (hasDeadline()) { + command.append(DEADLINE_WORD); + command.append(endDateTime.format(FORMATTER)); + } + return command.toString(); + } + + public int compareTo(TestTask other) { + int comparedCompletionStatus = compareCompletionStatus(other); + if (comparedCompletionStatus != 0) { + return comparedCompletionStatus; + } + + int comparedTaskTime = compareTaskTime(other); + if (!isCompleted() && comparedTaskTime != 0) { + return comparedTaskTime; + } + + int comparedLastUpdatedTime = compareLastUpdatedTime(other); + if (comparedLastUpdatedTime != 0) { + return comparedLastUpdatedTime; + } + + return compareName(other); + } + + public int compareCompletionStatus(TestTask other) { + return Boolean.compare(this.isCompleted(), other.isCompleted()); + } + + public int compareTaskTime(TestTask other) { + if (this.hasTime() && other.hasTime()) { + return this.getTaskTime().compareTo(other.getTaskTime()); + } else if (this.hasTime()) { + return -1; + } else if (other.hasTime()) { + return 1; + } else { + return 0; + } + } + + public int compareLastUpdatedTime(TestTask other) { + return other.getLastUpdatedTime().compareTo(this.getLastUpdatedTime()); + } + + public int compareName(TestTask other) { + return this.getName().toString().compareTo(other.getName().toString()); + } + + +} diff --git a/src/test/java/seedu/address/testutil/TestUtil.java b/src/test/java/seedu/agendum/testutil/TestUtil.java similarity index 56% rename from src/test/java/seedu/address/testutil/TestUtil.java rename to src/test/java/seedu/agendum/testutil/TestUtil.java index 17c92d66398a..738221826f3f 100644 --- a/src/test/java/seedu/address/testutil/TestUtil.java +++ b/src/test/java/seedu/agendum/testutil/TestUtil.java @@ -1,26 +1,4 @@ -package seedu.address.testutil; - -import com.google.common.io.Files; -import guitests.guihandles.PersonCardHandle; -import javafx.geometry.Bounds; -import javafx.geometry.Point2D; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; -import junit.framework.AssertionFailedError; -import org.loadui.testfx.GuiTest; -import org.testfx.api.FxToolkit; -import seedu.address.TestApp; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.XmlUtil; -import seedu.address.model.AddressBook; -import seedu.address.model.person.*; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; -import seedu.address.storage.XmlSerializableAddressBook; +package seedu.agendum.testutil; import java.io.File; import java.io.IOException; @@ -29,10 +7,39 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import org.loadui.testfx.GuiTest; +import org.testfx.api.FxToolkit; + +import com.google.common.io.Files; + +import guitests.guihandles.TaskCardHandle; +import javafx.geometry.Bounds; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import junit.framework.AssertionFailedError; +import seedu.agendum.TestApp; +import seedu.agendum.commons.core.Config; +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.commons.util.ConfigUtil; +import seedu.agendum.commons.util.FileUtil; +import seedu.agendum.commons.util.StringUtil; +import seedu.agendum.commons.util.XmlUtil; +import seedu.agendum.model.ToDoList; +import seedu.agendum.model.task.Name; +import seedu.agendum.model.task.ReadOnlyTask; +import seedu.agendum.model.task.Task; +import seedu.agendum.model.task.UniqueTaskList; +import seedu.agendum.storage.XmlSerializableToDoList; + /** * A utility class for test cases. */ @@ -60,20 +67,20 @@ public static void assertThrows(Class expected, Runnable ex */ public static String SANDBOX_FOLDER = FileUtil.getPath("./src/test/data/sandbox/"); - public static final Person[] samplePersonData = getSamplePersonData(); + public static final Task[] sampleTaskData = getSampleTaskData(); - private static Person[] getSamplePersonData() { + private static Task[] getSampleTaskData() { try { - return new Person[]{ - new Person(new Name("Ali Muster"), new Phone("9482424"), new Email("hans@google.com"), new Address("4th street"), new UniqueTagList()), - new Person(new Name("Boris Mueller"), new Phone("87249245"), new Email("ruth@google.com"), new Address("81th street"), new UniqueTagList()), - new Person(new Name("Carl Kurz"), new Phone("95352563"), new Email("heinz@yahoo.com"), new Address("wall street"), new UniqueTagList()), - new Person(new Name("Daniel Meier"), new Phone("87652533"), new Email("cornelia@google.com"), new Address("10th street"), new UniqueTagList()), - new Person(new Name("Elle Meyer"), new Phone("9482224"), new Email("werner@gmail.com"), new Address("michegan ave"), new UniqueTagList()), - new Person(new Name("Fiona Kunz"), new Phone("9482427"), new Email("lydia@gmail.com"), new Address("little tokyo"), new UniqueTagList()), - new Person(new Name("George Best"), new Phone("9482442"), new Email("anna@google.com"), new Address("4th street"), new UniqueTagList()), - new Person(new Name("Hoon Meier"), new Phone("8482424"), new Email("stefan@mail.com"), new Address("little india"), new UniqueTagList()), - new Person(new Name("Ida Mueller"), new Phone("8482131"), new Email("hans@google.com"), new Address("chicago ave"), new UniqueTagList()) + return new Task[]{ + new Task(new Name("Ali Muster")), + new Task(new Name("Boris Mueller")), + new Task(new Name("Carl Kurz")), + new Task(new Name("Daniel Meier")), + new Task(new Name("Elle Meyer")), + new Task(new Name("Fiona Kunz")), + new Task(new Name("George Best")), + new Task(new Name("Hoon Meier")), + new Task(new Name("Ida Mueller")) }; } catch (IllegalValueException e) { assert false; @@ -82,23 +89,8 @@ private static Person[] getSamplePersonData() { } } - public static final Tag[] sampleTagData = getSampleTagData(); - - private static Tag[] getSampleTagData() { - try { - return new Tag[]{ - new Tag("relatives"), - new Tag("friends") - }; - } catch (IllegalValueException e) { - assert false; - return null; - //not possible - } - } - - public static List generateSamplePersonData() { - return Arrays.asList(samplePersonData); + public static List generateSampleTaskData() { + return Arrays.asList(sampleTaskData); } /** @@ -111,13 +103,13 @@ public static String getFilePathInSandboxFolder(String fileName) { try { FileUtil.createDirs(new File(SANDBOX_FOLDER)); } catch (IOException e) { - throw new RuntimeException(e); + e.printStackTrace(); } return SANDBOX_FOLDER + fileName; } public static void createDataFileWithSampleData(String filePath) { - createDataFileWithData(generateSampleStorageAddressBook(), filePath); + createDataFileWithData(generateSampleStorageToDoList(), filePath); } public static void createDataFileWithData(T data, String filePath) { @@ -126,7 +118,7 @@ public static void createDataFileWithData(T data, String filePath) { FileUtil.createIfMissing(saveFileForTesting); XmlUtil.saveDataToFile(saveFileForTesting, data); } catch (Exception e) { - throw new RuntimeException(e); + e.printStackTrace(); } } @@ -134,12 +126,12 @@ public static void main(String... s) { createDataFileWithSampleData(TestApp.SAVE_LOCATION_FOR_TESTING); } - public static AddressBook generateEmptyAddressBook() { - return new AddressBook(new UniquePersonList(), new UniqueTagList()); + public static ToDoList generateEmptyToDoList() { + return new ToDoList(new UniqueTaskList()); } - public static XmlSerializableAddressBook generateSampleStorageAddressBook() { - return new XmlSerializableAddressBook(generateEmptyAddressBook()); + public static XmlSerializableToDoList generateSampleStorageToDoList() { + return new XmlSerializableToDoList(generateEmptyToDoList()); } /** @@ -273,82 +265,115 @@ public static Object getLastElement(List list) { } /** - * Removes a subset from the list of persons. - * @param persons The list of persons - * @param personsToRemove The subset of persons. - * @return The modified persons after removal of the subset from persons. + * Removes a subset from the list of tasks. + * @param tasks The list of tasks + * @param tasksToRemove The subset of tasks. + * @return The modified tasks after removal of the subset from tasks. */ - public static TestPerson[] removePersonsFromList(final TestPerson[] persons, TestPerson... personsToRemove) { - List listOfPersons = asList(persons); - listOfPersons.removeAll(asList(personsToRemove)); - return listOfPersons.toArray(new TestPerson[listOfPersons.size()]); + public static TestTask[] removeTasksFromList(final TestTask[] tasks, TestTask... tasksToRemove) { + List listOfTasks = asList(tasks); + listOfTasks.removeAll(asList(tasksToRemove)); + return listOfTasks.toArray(new TestTask[listOfTasks.size()]); } /** - * Returns a copy of the list with the person at specified index removed. + * Returns a copy of the list with the task at specified index removed. * @param list original list to copy from * @param targetIndexInOneIndexedFormat e.g. if the first element to be removed, 1 should be given as index. */ - public static TestPerson[] removePersonFromList(final TestPerson[] list, int targetIndexInOneIndexedFormat) { - return removePersonsFromList(list, list[targetIndexInOneIndexedFormat-1]); + public static TestTask[] removeTaskFromList(final TestTask[] list, int targetIndexInOneIndexedFormat) { + return removeTasksFromList(list, list[targetIndexInOneIndexedFormat-1]); } /** - * Replaces persons[i] with a person. - * @param persons The array of persons. - * @param person The replacement person - * @param index The index of the person to be replaced. + * Replaces tasks[i] with a task. + * @param tasks The array of tasks. + * @param task The replacement task + * @param index The index of the task to be replaced. * @return */ - public static TestPerson[] replacePersonFromList(TestPerson[] persons, TestPerson person, int index) { - persons[index] = person; - return persons; + public static TestTask[] replaceTaskFromList(TestTask[] tasks, TestTask task, int index) { + tasks[index] = task; + return tasks; } /** - * Appends persons to the array of persons. - * @param persons A array of persons. - * @param personsToAdd The persons that are to be appended behind the original array. - * @return The modified array of persons. + * Appends tasks to the array of tasks. + * @param tasks A array of tasks. + * @param tasksToAdd The tasks that are to be appended behind the original array. + * @return The modified array of tasks. */ - public static TestPerson[] addPersonsToList(final TestPerson[] persons, TestPerson... personsToAdd) { - List listOfPersons = asList(persons); - listOfPersons.addAll(asList(personsToAdd)); - return listOfPersons.toArray(new TestPerson[listOfPersons.size()]); + public static TestTask[] addTasksToList(final TestTask[] tasks, TestTask... tasksToAdd) { + List listOfTasks = asList(tasks); + listOfTasks.addAll(asList(tasksToAdd)); + TestTask[] newTasks = listOfTasks.toArray(new TestTask[listOfTasks.size()]); + return newTasks; } private static List asList(T[] objs) { List list = new ArrayList<>(); - for(T obj : objs) { - list.add(obj); - } + Collections.addAll(list, objs); return list; } - public static boolean compareCardAndPerson(PersonCardHandle card, ReadOnlyPerson person) { - return card.isSamePerson(person); + public static void sortTasks(TestTask[] tasks) { + Arrays.sort(tasks); } - public static Tag[] getTagList(String tags) { - - if (tags.equals("")) { - return new Tag[]{}; + public static TestTask[] getUpcomingTasks(TestTask[] tasks) { + ArrayList filteredTasks = new ArrayList(); + for (TestTask task: tasks) { + if (!task.isCompleted() && task.hasTime()) { + filteredTasks.add(task); + } } + return filteredTasks.toArray(new TestTask[filteredTasks.size()]); + } - final String[] split = tags.split(", "); - - final List collect = Arrays.asList(split).stream().map(e -> { - try { - return new Tag(e.replaceFirst("Tag: ", "")); - } catch (IllegalValueException e1) { - //not possible - assert false; - return null; + public static TestTask[] getFloatingTasks(TestTask[] tasks) { + ArrayList filteredTasks = new ArrayList(); + for (TestTask task: tasks) { + if (!task.isCompleted() && !task.hasTime()) { + filteredTasks.add(task); } - }).collect(Collectors.toList()); + } + return filteredTasks.toArray(new TestTask[filteredTasks.size()]); + } - return collect.toArray(new Tag[split.length]); + public static TestTask[] getCompletedTasks(TestTask[] tasks) { + ArrayList filteredTasks = new ArrayList(); + for (TestTask task: tasks) { + if (task.isCompleted()) { + filteredTasks.add(task); + } + } + return filteredTasks.toArray(new TestTask[filteredTasks.size()]); + } + + public static boolean compareCardAndTask(TaskCardHandle card, ReadOnlyTask task) { + return card.isSameTask(task); + } + + public static Config createTempConfig() { + Config config = new Config(); + final String DEFAULT_CONFIG_FILE_LOCATION_FOR_TESTING = getFilePathInSandboxFolder("config_testing.json"); + final String DEFAULT_PREF_FILE_LOCATION_FOR_TESTING = TestUtil.getFilePathInSandboxFolder("pref_testing.json"); + final String DEFAULT_TODOLIST_FILE_LOCATION_FOR_TESTING = TestUtil.getFilePathInSandboxFolder("todolist_testing.xml"); + final String DEFAULT_ALIAS_TABLE_FILE_LOCATION_FOR_TESTING = TestUtil.getFilePathInSandboxFolder("todolist_testing.xml"); + + config.setAppTitle(TestApp.APP_TITLE); + config.setUserPrefsFilePath(DEFAULT_PREF_FILE_LOCATION_FOR_TESTING); + config.setToDoListFilePath(DEFAULT_TODOLIST_FILE_LOCATION_FOR_TESTING); + config.setAliasTableFilePath(DEFAULT_ALIAS_TABLE_FILE_LOCATION_FOR_TESTING); + config.setConfigFilePath(DEFAULT_CONFIG_FILE_LOCATION_FOR_TESTING); + + try { + ConfigUtil.saveConfig(config, DEFAULT_CONFIG_FILE_LOCATION_FOR_TESTING); + } catch (IOException e) { + System.out.println("Failed to save config file : " + StringUtil.getDetails(e)); + } + return config; } } diff --git a/src/test/java/seedu/agendum/testutil/ToDoListBuilder.java b/src/test/java/seedu/agendum/testutil/ToDoListBuilder.java new file mode 100644 index 000000000000..f89e21376233 --- /dev/null +++ b/src/test/java/seedu/agendum/testutil/ToDoListBuilder.java @@ -0,0 +1,28 @@ +package seedu.agendum.testutil; + +import seedu.agendum.model.task.Task; +import seedu.agendum.model.task.UniqueTaskList; +import seedu.agendum.model.ToDoList; + +/** + * A utility class to help with building ToDoList objects. + * Example usage:
+ * {@code ToDoList ab = new ToDoListBuilder().withTask("John", "Doe").withTag("Friend").build();} + */ +public class ToDoListBuilder { + + private ToDoList toDoList; + + public ToDoListBuilder(ToDoList toDoList){ + this.toDoList = toDoList; + } + + public ToDoListBuilder withTask(Task task) throws UniqueTaskList.DuplicateTaskException { + toDoList.addTask(task); + return this; + } + + public ToDoList build(){ + return toDoList; + } +} diff --git a/src/test/java/seedu/agendum/testutil/TypicalTestTasks.java b/src/test/java/seedu/agendum/testutil/TypicalTestTasks.java new file mode 100644 index 000000000000..e501e400b73d --- /dev/null +++ b/src/test/java/seedu/agendum/testutil/TypicalTestTasks.java @@ -0,0 +1,74 @@ +package seedu.agendum.testutil; + +import java.time.LocalDateTime; + +import seedu.agendum.commons.exceptions.IllegalValueException; +import seedu.agendum.model.ToDoList; +import seedu.agendum.model.task.Task; + +public class TypicalTestTasks { + + public static final TestTask ALICE = generateTaskWithName("meet Alice Pauline"); + public static final TestTask BENSON = generateTaskWithName("meet Benson Meier"); + public static final TestTask CARL = generateTaskWithName("meet Carl Kurz"); + public static final TestTask DANIEL = generateTaskWithName("meet Daniel Meier"); + public static final TestTask ELLE = generateTaskWithName("meet Elle Meyer"); + public static final TestTask FIONA = generateTaskWithName("meet Fiona Kunz"); + public static final TestTask GEORGE = generateTaskWithName("meet George Best"); + public static final TestTask HOON = generateTaskWithName("meet Hoon Meier"); + public static final TestTask IDA = generateTaskWithName("meet Ida Mueller"); + + private static final LocalDateTime yesterday = LocalDateTime.now().minusDays(1); + private static final LocalDateTime tomorrow = LocalDateTime.now().plusDays(1); + + + public static void loadToDoListWithSampleData(ToDoList tdl) { + try { + tdl.addTask(new Task(ALICE)); + tdl.addTask(new Task(BENSON)); + tdl.addTask(new Task(CARL)); + tdl.addTask(new Task(DANIEL)); + tdl.addTask(new Task(ELLE)); + tdl.addTask(new Task(FIONA)); + tdl.addTask(new Task(GEORGE)); + } catch (IllegalValueException e) { + assert false : "not possible"; + } + } + + private static TestTask generateTaskWithName(String name) { + try { + return new TaskBuilder().withName(name).withUncompletedStatus().build(); + } catch (IllegalValueException ive) { + assert false: "Not possible"; + return null; + } + } + + public static TestTask getEventTestTask() throws IllegalValueException { + return new TaskBuilder().withName("meeting") + .withUncompletedStatus() + .withStartTime(yesterday) + .withEndTime(tomorrow).build(); + } + + public static TestTask getDeadlineTestTask() throws IllegalValueException { + return new TaskBuilder().withName("due soon") + .withUncompletedStatus() + .withEndTime(tomorrow).build(); + } + + public static TestTask getFloatingTestTask() throws IllegalValueException { + return new TaskBuilder().withName("anytime").withUncompletedStatus().build(); + } + + public TestTask[] getTypicalTasks() { + return new TestTask[]{ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE}; + } + + public ToDoList getTypicalToDoList(){ + ToDoList tdl = new ToDoList(); + loadToDoListWithSampleData(tdl); + return tdl; + } +}