├── _config.yml ├── test ├── exitinput.txt ├── runtests.bat ├── runtests.sh ├── input.txt └── expected.txt ├── .gitignore ├── LICENSE ├── README.md └── src └── seedu └── addressbook └── AddressBook.java /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /test/exitinput.txt: -------------------------------------------------------------------------------- 1 | # exit directly 2 | exit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Default storage file for addressbook : don't need to cleanup when running from IDE 2 | addressbook.txt 3 | 4 | # Compiled classfiles 5 | *.class 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | 15 | # IDEA files 16 | .idea/ 17 | *.iml 18 | 19 | # Temp files used for testing 20 | test/actual.txt 21 | test/localrun.bat 22 | test/data/ 23 | /bin/ 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 nus-oss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/runtests.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM create bin directory if it doesn't exist 4 | if not exist ..\bin mkdir ..\bin 5 | 6 | REM compile the code into the bin folder 7 | javac ..\src\seedu\addressbook\Addressbook.java -d ..\bin 8 | 9 | REM (invalid) no parent directory, invalid filename with no extension 10 | java -classpath ..\bin seedu.addressbook.AddressBook " " < NUL > actual.txt 11 | REM (invalid) invalid parent directory that does not exist, valid filename 12 | java -classpath ..\bin seedu.addressbook.AddressBook "directoryThatDoesNotExist/valid.filename" < NUL >> actual.txt 13 | REM (invalid) no parent directory, invalid filename with dot on first character 14 | java -classpath ..\bin seedu.addressbook.AddressBook ".noFilename" < NUL >> actual.txt 15 | REM (invalid) valid parent directory, non regular file 16 | if not exist data\notRegularFile.txt mkdir data\notRegularFile.txt 17 | java -classpath ..\bin seedu.addressbook.AddressBook "data/notRegularFile.txt" < NUL >> actual.txt 18 | REM (valid) valid parent directory, valid filename with extension. 19 | copy /y NUL data\valid.filename 20 | java -classpath ..\bin seedu.addressbook.AddressBook "data/valid.filename" < exitinput.txt >> actual.txt 21 | REM run the program, feed commands from input.txt file and redirect the output to the actual.txt 22 | java -classpath ..\bin seedu.addressbook.AddressBook < input.txt >> actual.txt 23 | 24 | REM compare the output to the expected output 25 | FC actual.txt expected.txt -------------------------------------------------------------------------------- /test/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # change to script directory 4 | cd "${0%/*}" 5 | 6 | # create ../bin directory if not exists 7 | if [ ! -d "../bin" ] 8 | then 9 | mkdir ../bin 10 | fi 11 | 12 | # compile the code into the bin folder 13 | javac ../src/seedu/addressbook/AddressBook.java -d ../bin 14 | 15 | # (invalid) no parent directory, invalid filename with no extension 16 | java -classpath ../bin seedu.addressbook.AddressBook ' ' < /dev/null > actual.txt 17 | # (invalid) invalid parent directory that does not exist, valid filename 18 | java -classpath ../bin seedu.addressbook.AddressBook 'directoryThatDoesNotExist/valid.filename' < /dev/null >> actual.txt 19 | # (invalid) no parent directory, invalid filename with dot on first character 20 | java -classpath ../bin seedu.addressbook.AddressBook '.noFilename' < /dev/null >> actual.txt 21 | # (invalid) valid parent directory, non regular file 22 | mkdir -p data/notRegularFile.txt 23 | java -classpath ../bin seedu.addressbook.AddressBook 'data/notRegularFile.txt' < /dev/null >> actual.txt 24 | # (valid) valid parent directory, valid filename with extension. 25 | touch data/valid.filename 26 | java -classpath ../bin seedu.addressbook.AddressBook 'data/valid.filename' < exitinput.txt >> actual.txt 27 | # run the program, feed commands from input.txt file and redirect the output to the actual.txt 28 | java -classpath ../bin seedu.addressbook.AddressBook < input.txt >> actual.txt 29 | 30 | # compare the output to the expected output 31 | diff actual.txt expected.txt 32 | if [ $? -eq 0 ] 33 | then 34 | echo "Test result: PASSED" 35 | else 36 | echo "Test result: FAILED" 37 | fi 38 | -------------------------------------------------------------------------------- /test/input.txt: -------------------------------------------------------------------------------- 1 | ########################################################## 2 | # invalid command 3 | ########################################################## 4 | 5 | # should recognise invalid command 6 | sfdfd 7 | 8 | ########################################################## 9 | # clean and check state 10 | ########################################################## 11 | 12 | # address book should be emptied 13 | clear 14 | list 15 | 16 | ########################################################## 17 | # test add person command, setup state for future tests 18 | ########################################################## 19 | 20 | # should catch invalid args format 21 | add wrong args wrong args 22 | add Valid Name p/12345 valid@email.butNoPrefix 23 | add Valid Name 12345 e/valid@email.butPhonePrefixMissing 24 | 25 | # should catch invalid person data 26 | add []\[;] p/12345 e/valid@e.mail 27 | add Valid Name p/not_numbers e/valid@e.mail 28 | add Valid Name p/12345 e/notAnEmail 29 | 30 | # should add correctly 31 | add Adam Brown p/111111 e/adam@gmail.com 32 | list 33 | add Betsy Choo p/222222 e/benchoo@nus.edu.sg 34 | list 35 | 36 | # order of phone and email should not matter 37 | add Charlie Dickson e/charlie.d@nus.edu.sg p/333333 38 | list 39 | add Dickson Ee e/dickson@nus.edu.sg p/444444 40 | list 41 | add Esther Potato p/555555 e/esther@notreal.potato 42 | list 43 | 44 | ########################################################## 45 | # test find persons command 46 | ########################################################## 47 | 48 | # should match none with no keywords 49 | find 50 | # should only match full words in person names 51 | find bet 52 | # does not match if none have keyword 53 | find 23912039120 54 | 55 | # matching should be case-insensitive 56 | find betsy 57 | # find unique keyword 58 | find Betsy 59 | # find multiple with same keyword 60 | find Dickson 61 | # find multiple with some keywords 62 | find Charlie Betsy 63 | 64 | ########################################################## 65 | # test delete person command 66 | ########################################################## 67 | 68 | # last active view: [1] betsy [2] charlie 69 | 70 | # should catch invalid args format 71 | delete 72 | delete should be only one number 73 | 74 | # should catch invalid index 75 | delete -1 76 | delete 0 77 | delete 3 78 | 79 | # should catch attempt to delete something already deleted 80 | delete 2 81 | delete 2 82 | 83 | # should have deleted based on last active view's index 84 | list 85 | 86 | # deletes correct person 87 | delete 4 88 | list 89 | 90 | # listing indexes get updated on next request 91 | delete 1 92 | list 93 | 94 | ########################################################## 95 | # test clear command 96 | ########################################################## 97 | 98 | # clears all 99 | clear 100 | list 101 | 102 | ########################################################## 103 | # test exit command 104 | ########################################################## 105 | 106 | # exits properly 107 | exit 108 | list -------------------------------------------------------------------------------- /test/expected.txt: -------------------------------------------------------------------------------- 1 | ][ =================================================== 2 | ][ =================================================== 3 | ][ AddessBook Level 1 - Version 1.0 4 | ][ Welcome to your Address Book! 5 | ][ =================================================== 6 | ][ The given file name [ ] is not a valid file name! 7 | ][ Exiting Address Book... Good bye! 8 | ][ =================================================== 9 | ][ =================================================== 10 | ][ =================================================== 11 | ][ =================================================== 12 | ][ AddessBook Level 1 - Version 1.0 13 | ][ Welcome to your Address Book! 14 | ][ =================================================== 15 | ][ The given file name [directoryThatDoesNotExist/valid.filename] is not a valid file name! 16 | ][ Exiting Address Book... Good bye! 17 | ][ =================================================== 18 | ][ =================================================== 19 | ][ =================================================== 20 | ][ =================================================== 21 | ][ AddessBook Level 1 - Version 1.0 22 | ][ Welcome to your Address Book! 23 | ][ =================================================== 24 | ][ The given file name [.noFilename] is not a valid file name! 25 | ][ Exiting Address Book... Good bye! 26 | ][ =================================================== 27 | ][ =================================================== 28 | ][ =================================================== 29 | ][ =================================================== 30 | ][ AddessBook Level 1 - Version 1.0 31 | ][ Welcome to your Address Book! 32 | ][ =================================================== 33 | ][ The given file name [data/notRegularFile.txt] is not a valid file name! 34 | ][ Exiting Address Book... Good bye! 35 | ][ =================================================== 36 | ][ =================================================== 37 | ][ =================================================== 38 | ][ =================================================== 39 | ][ AddessBook Level 1 - Version 1.0 40 | ][ Welcome to your Address Book! 41 | ][ =================================================== 42 | ][ Enter command: ][ [Command entered:exit] 43 | ][ Exiting Address Book... Good bye! 44 | ][ =================================================== 45 | ][ =================================================== 46 | ][ =================================================== 47 | ][ =================================================== 48 | ][ AddessBook Level 1 - Version 1.0 49 | ][ Welcome to your Address Book! 50 | ][ =================================================== 51 | ][ Using default storage file : addressbook.txt 52 | ][ Enter command: ][ [Command entered: sfdfd] 53 | ][ Invalid command format: sfdfd 54 | ][ add: Adds a person to the address book. 55 | ][ Parameters: NAME p/PHONE_NUMBER e/EMAIL 56 | ][ Example: add John Doe p/98765432 e/johnd@gmail.com 57 | ][ 58 | ][ find: Finds all persons whose names contain any of the specified keywords (case-sensitive) and displays them as a list with index numbers. 59 | ][ Parameters: KEYWORD [MORE_KEYWORDS] 60 | ][ Example: find alice bob charlie 61 | ][ 62 | ][ list: Displays all persons as a list with index numbers. 63 | ][ Example: list 64 | ][ 65 | ][ delete: Deletes a person identified by the index number used in the last find/list call. 66 | ][ Parameters: INDEX 67 | ][ Example: delete 1 68 | ][ 69 | ][ clear: Clears address book permanently. 70 | ][ Example: clear 71 | ][ 72 | ][ exit: Exits the program. Example: exit 73 | ][ help: Shows program usage instructions. Example: help 74 | ][ =================================================== 75 | ][ Enter command: ][ [Command entered: clear] 76 | ][ Address book has been cleared! 77 | ][ =================================================== 78 | ][ Enter command: ][ [Command entered: list] 79 | ][ 80 | ][ 0 persons found! 81 | ][ =================================================== 82 | ][ Enter command: ][ [Command entered: add wrong args wrong args] 83 | ][ Invalid command format: add 84 | ][ add: Adds a person to the address book. 85 | ][ Parameters: NAME p/PHONE_NUMBER e/EMAIL 86 | ][ Example: add John Doe p/98765432 e/johnd@gmail.com 87 | ][ 88 | ][ =================================================== 89 | ][ Enter command: ][ [Command entered: add Valid Name p/12345 valid@email.butNoPrefix] 90 | ][ Invalid command format: add 91 | ][ add: Adds a person to the address book. 92 | ][ Parameters: NAME p/PHONE_NUMBER e/EMAIL 93 | ][ Example: add John Doe p/98765432 e/johnd@gmail.com 94 | ][ 95 | ][ =================================================== 96 | ][ Enter command: ][ [Command entered: add Valid Name 12345 e/valid@email.butPhonePrefixMissing] 97 | ][ Invalid command format: add 98 | ][ add: Adds a person to the address book. 99 | ][ Parameters: NAME p/PHONE_NUMBER e/EMAIL 100 | ][ Example: add John Doe p/98765432 e/johnd@gmail.com 101 | ][ 102 | ][ =================================================== 103 | ][ Enter command: ][ [Command entered: add []\[;] p/12345 e/valid@e.mail] 104 | ][ Invalid command format: add 105 | ][ add: Adds a person to the address book. 106 | ][ Parameters: NAME p/PHONE_NUMBER e/EMAIL 107 | ][ Example: add John Doe p/98765432 e/johnd@gmail.com 108 | ][ 109 | ][ =================================================== 110 | ][ Enter command: ][ [Command entered: add Valid Name p/not_numbers e/valid@e.mail] 111 | ][ Invalid command format: add 112 | ][ add: Adds a person to the address book. 113 | ][ Parameters: NAME p/PHONE_NUMBER e/EMAIL 114 | ][ Example: add John Doe p/98765432 e/johnd@gmail.com 115 | ][ 116 | ][ =================================================== 117 | ][ Enter command: ][ [Command entered: add Valid Name p/12345 e/notAnEmail] 118 | ][ Invalid command format: add 119 | ][ add: Adds a person to the address book. 120 | ][ Parameters: NAME p/PHONE_NUMBER e/EMAIL 121 | ][ Example: add John Doe p/98765432 e/johnd@gmail.com 122 | ][ 123 | ][ =================================================== 124 | ][ Enter command: ][ [Command entered: add Adam Brown p/111111 e/adam@gmail.com] 125 | ][ New person added: Adam Brown, Phone: 111111, Email: adam@gmail.com 126 | ][ =================================================== 127 | ][ Enter command: ][ [Command entered: list] 128 | ][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com 129 | ][ 130 | ][ 1 persons found! 131 | ][ =================================================== 132 | ][ Enter command: ][ [Command entered: add Betsy Choo p/222222 e/benchoo@nus.edu.sg] 133 | ][ New person added: Betsy Choo, Phone: 222222, Email: benchoo@nus.edu.sg 134 | ][ =================================================== 135 | ][ Enter command: ][ [Command entered: list] 136 | ][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com 137 | ][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 138 | ][ 139 | ][ 2 persons found! 140 | ][ =================================================== 141 | ][ Enter command: ][ [Command entered: add Charlie Dickson e/charlie.d@nus.edu.sg p/333333] 142 | ][ New person added: Charlie Dickson, Phone: 333333, Email: charlie.d@nus.edu.sg 143 | ][ =================================================== 144 | ][ Enter command: ][ [Command entered: list] 145 | ][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com 146 | ][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 147 | ][ 3. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg 148 | ][ 149 | ][ 3 persons found! 150 | ][ =================================================== 151 | ][ Enter command: ][ [Command entered: add Dickson Ee e/dickson@nus.edu.sg p/444444] 152 | ][ New person added: Dickson Ee, Phone: 444444, Email: dickson@nus.edu.sg 153 | ][ =================================================== 154 | ][ Enter command: ][ [Command entered: list] 155 | ][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com 156 | ][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 157 | ][ 3. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg 158 | ][ 4. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg 159 | ][ 160 | ][ 4 persons found! 161 | ][ =================================================== 162 | ][ Enter command: ][ [Command entered: add Esther Potato p/555555 e/esther@notreal.potato] 163 | ][ New person added: Esther Potato, Phone: 555555, Email: esther@notreal.potato 164 | ][ =================================================== 165 | ][ Enter command: ][ [Command entered: list] 166 | ][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com 167 | ][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 168 | ][ 3. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg 169 | ][ 4. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg 170 | ][ 5. Esther Potato Phone Number: 555555 Email: esther@notreal.potato 171 | ][ 172 | ][ 5 persons found! 173 | ][ =================================================== 174 | ][ Enter command: ][ [Command entered: find] 175 | ][ 176 | ][ 0 persons found! 177 | ][ =================================================== 178 | ][ Enter command: ][ [Command entered: find bet] 179 | ][ 180 | ][ 0 persons found! 181 | ][ =================================================== 182 | ][ Enter command: ][ [Command entered: find 23912039120] 183 | ][ 184 | ][ 0 persons found! 185 | ][ =================================================== 186 | ][ Enter command: ][ [Command entered: find betsy] 187 | ][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 188 | ][ 189 | ][ 1 persons found! 190 | ][ =================================================== 191 | ][ Enter command: ][ [Command entered: find Betsy] 192 | ][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 193 | ][ 194 | ][ 1 persons found! 195 | ][ =================================================== 196 | ][ Enter command: ][ [Command entered: find Dickson] 197 | ][ 1. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg 198 | ][ 2. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg 199 | ][ 200 | ][ 2 persons found! 201 | ][ =================================================== 202 | ][ Enter command: ][ [Command entered: find Charlie Betsy] 203 | ][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 204 | ][ 2. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg 205 | ][ 206 | ][ 2 persons found! 207 | ][ =================================================== 208 | ][ Enter command: ][ [Command entered: delete] 209 | ][ Invalid command format: delete 210 | ][ delete: Deletes a person identified by the index number used in the last find/list call. 211 | ][ Parameters: INDEX 212 | ][ Example: delete 1 213 | ][ 214 | ][ =================================================== 215 | ][ Enter command: ][ [Command entered: delete should be only one number] 216 | ][ Invalid command format: delete 217 | ][ delete: Deletes a person identified by the index number used in the last find/list call. 218 | ][ Parameters: INDEX 219 | ][ Example: delete 1 220 | ][ 221 | ][ =================================================== 222 | ][ Enter command: ][ [Command entered: delete -1] 223 | ][ Invalid command format: delete 224 | ][ delete: Deletes a person identified by the index number used in the last find/list call. 225 | ][ Parameters: INDEX 226 | ][ Example: delete 1 227 | ][ 228 | ][ =================================================== 229 | ][ Enter command: ][ [Command entered: delete 0] 230 | ][ Invalid command format: delete 231 | ][ delete: Deletes a person identified by the index number used in the last find/list call. 232 | ][ Parameters: INDEX 233 | ][ Example: delete 1 234 | ][ 235 | ][ =================================================== 236 | ][ Enter command: ][ [Command entered: delete 3] 237 | ][ The person index provided is invalid 238 | ][ =================================================== 239 | ][ Enter command: ][ [Command entered: delete 2] 240 | ][ Deleted Person: Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg 241 | ][ =================================================== 242 | ][ Enter command: ][ [Command entered: delete 2] 243 | ][ Person could not be found in address book 244 | ][ =================================================== 245 | ][ Enter command: ][ [Command entered: list] 246 | ][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com 247 | ][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 248 | ][ 3. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg 249 | ][ 4. Esther Potato Phone Number: 555555 Email: esther@notreal.potato 250 | ][ 251 | ][ 4 persons found! 252 | ][ =================================================== 253 | ][ Enter command: ][ [Command entered: delete 4] 254 | ][ Deleted Person: Esther Potato Phone Number: 555555 Email: esther@notreal.potato 255 | ][ =================================================== 256 | ][ Enter command: ][ [Command entered: list] 257 | ][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com 258 | ][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 259 | ][ 3. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg 260 | ][ 261 | ][ 3 persons found! 262 | ][ =================================================== 263 | ][ Enter command: ][ [Command entered: delete 1] 264 | ][ Deleted Person: Adam Brown Phone Number: 111111 Email: adam@gmail.com 265 | ][ =================================================== 266 | ][ Enter command: ][ [Command entered: list] 267 | ][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg 268 | ][ 2. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg 269 | ][ 270 | ][ 2 persons found! 271 | ][ =================================================== 272 | ][ Enter command: ][ [Command entered: clear] 273 | ][ Address book has been cleared! 274 | ][ =================================================== 275 | ][ Enter command: ][ [Command entered: list] 276 | ][ 277 | ][ 0 persons found! 278 | ][ =================================================== 279 | ][ Enter command: ][ [Command entered: exit] 280 | ][ Exiting Address Book... Good bye! 281 | ][ =================================================== 282 | ][ =================================================== 283 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AddressBook (Level 1) 2 | * This is a CLI (Command Line Interface) Address Book application **written in procedural fashion**. 3 | * It is a Java sample application intended for students learning Software Engineering while using Java as 4 | the main programming language. 5 | * It provides a **reasonably well-written** code example that is **significantly bigger** than what students 6 | usually write in data structure modules. 7 | * It can be used to achieve a number of beginner-level [learning outcomes](#learning-outcomes) without 8 | running into the complications of OOP or GUI programmings. 9 | 10 | **Table of Contents** 11 | * [**User Guide**](#user-guide) 12 | * [Starting the program](#starting-the-program) 13 | * [List of commands](#list-of-commands) 14 | * [**Developer Guide**](#developer-guide) 15 | * [Setting Up](#setting-up) 16 | * [Design](#design) 17 | * [Testing](#testing) 18 | * [**Learning Outcomes**](#learning-outcomes) 19 | 1. [Set up a project in an IDE [`LO-IdeSetup`]](#set-up-a-project-in-an-ide-lo-idesetup) 20 | 2. [Navigate code efficiently [`LO-CodeNavigation`]](#navigate-code-efficiently-lo-codenavigation) 21 | 3. [Use a debugger [`LO-Debugging`]](#use-a-debugger-lo-debugging) 22 | 4. [Automate CLI testing [`LO-AutomatedCliTesting`]](#automate-cli-testing-lo-automatedclitesting) 23 | 5. [Use Collections [`LO-Collections`]](#use-collections-lo-collections) 24 | 6. [Use Enums [`LO-Enums`]](#use-enums-lo-enums) 25 | 7. [Use Varargs [`LO-Varargs`]](#use-varargs-lo-varargs) 26 | 8. [Follow a coding standard [`LO-CodingStandard`]](#follow-a-coding-standard-lo-codingstandard) 27 | 9. [Apply coding best practices [`LO-CodingBestPractices`]](#apply-coding-best-practices-lo-codingbestpractices) 28 | 10. [Refactor code [`LO-Refactor`]](#refactor-code-lo-refactor) 29 | 11. [Abstract methods well [`LO-MethodAbstraction`]](#abstract-methods-well-lo-methodabstraction) 30 | 12. [Follow SLAP [`LO-SLAP`]](#follow-slap-lo-slap) 31 | 13. [Work in a 1kLoC code base [`LO-1KLoC`]](#work-in-a-1kloc-code-baselo-1kloc) 32 | * [**Contributors**](#contributors) 33 | * [**Contact Us**](#contact-us) 34 | 35 | ----------------------------------------------------------------------------------------------------- 36 | # User Guide 37 | 38 | This product is not meant for end-users and therefore there is no user-friendly installer. 39 | Please refer to the [Setting up](#setting-up) section to learn how to set up the project. 40 | 41 | ## Starting the program 42 | 43 | **Using IntelliJ** 44 | 45 | 1. Find the project in the `Project Explorer` (usually located at the left side) 46 | 1. If the `Project Explorer` is not visible, press ALT+1 for Windows/Linux, CMD+1 for macOS to open the `Project Explorer` tab 47 | 2. Go to the `src` folder and locate the `AddressBook` file 48 | 3. Right click the file and select `Run AddressBook.main()` 49 | 4. The program now should run on the `Console` (usually located at the bottom side) 50 | 5. Now you can interact with the program through the `Console` 51 | 52 | **Using Command Line** 53 | 54 | 1. 'Build' the project using IntelliJ (`Build` -> `Build Project`) 55 | 2. Open the `Terminal`/`Command Prompt`. Note: You can open a terminal inside Intellij too (`View` -> `Tool Windows` -> `Terminal`) 56 | 3. `cd` into the project's `out\production\addressbook-level1` directory. Note: That is the where Intellij puts its compiled class files. 57 | 4. Type `java seedu.addressbook.AddressBook`, then Enter to execute 58 | 5. Now you can interact with the program through the CLI 59 | 60 | ## List of commands 61 | #### Viewing help: `help` 62 | Format: `help` 63 | > Help is also shown if you enter an incorrect command e.g. `abcd` 64 | 65 | #### Adding a person: `add` 66 | > Adds a person to the address book 67 | 68 | Format: `add NAME p/PHONE_NUMBER e/EMAIL` 69 | > Words in `UPPER_CASE` are the parameters
70 | Phone number and email can be in any order but the name must come first. 71 | 72 | Examples: 73 | * `add John Doe p/98765432 e/johnd@gmail.com` 74 | * `add Betsy Crowe e/bencrowe@gmail.com p/1234567 ` 75 | 76 | #### Listing all persons: `list` 77 | 78 | > Shows a list of persons, as an indexed list, in the order they were added to the address book, 79 | oldest first. 80 | 81 | Format: `list` 82 | 83 | #### Finding a person by keyword `find` 84 | > Finds persons that match given keywords 85 | 86 | Format: `find KEYWORD [MORE_KEYWORDS]` 87 | > The search is case insensitive, the order of the keywords does not matter, only the name is searched, 88 | and persons matching at least one keyword will be returned (i.e. `OR` search). 89 | 90 | Examples: 91 | * `find John` 92 | > Returns `John Doe` but not `john` 93 | 94 | * `find Betsy Tim John` 95 | > Returns Any person having names `Betsy`, `Tim`, or `John` 96 | 97 | #### Deleting a person: `delete` 98 | 99 | Format: `delete INDEX` 100 | > Deletes the person at the specified `INDEX`. 101 | The index refers to the index numbers shown in the most recent listing. 102 | 103 | Examples: 104 | * `list`
105 | `delete 2` 106 | > Deletes the 2nd person in the address book. 107 | 108 | * `find Betsy`
109 | `delete 1` 110 | > Deletes the 1st person in the results of the `find` command. 111 | 112 | #### Clearing all entries: `clear` 113 | > Clears all entries from the address book. 114 | Format: `clear` 115 | 116 | #### Exiting the program: `exit` 117 | Format: `exit` 118 | 119 | #### Saving the data 120 | Address book data are saved in the hard disk automatically after any command that changes the data. 121 | There is no need to save manually. 122 | 123 | #### Changing the save location 124 | Address book data are saved in a file called `addressbook.txt` in the project root folder. 125 | You can change the location by specifying the file path as a program argument. 126 | 127 | Example: 128 | 129 | * `java seedu.addressbook.AddressBook mydata.txt` 130 | * `java seedu.addressbook.AddressBook myFolder/mydata.txt` 131 | 132 | > The file path must contain a valid file name and a valid parent directory.
133 | File name is valid if it has an extension and no reserved characters (OS-dependent).
134 | Parent directory is valid if it exists.
135 | Note for non-Windows users: if the file already exists, it must be a 'regular' file.
136 | 137 | > When running the program inside IntelliJ, there is a way to set command line parameters 138 | before running the program. 139 | 140 | ----------------------------------------------------------------------------------------------------- 141 | # Developer Guide 142 | 143 | ## Setting up 144 | 145 | **Prerequisites** 146 | 147 | * JDK 8 or later 148 | * IntelliJ IDE 149 | 150 | **Importing the project into IntelliJ** 151 | 152 | 1. Open IntelliJ (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project dialog first) 153 | 2. Set up the correct JDK version 154 | 1. Click `Configure` > `Project Defaults` > `Project Structure` 155 | 2. If JDK 8 is listed in the drop down, select it. If it is not, click `New...` and select the directory where you installed JDK 8. 156 | 3. Click `OK`. 157 | 3. Click `Import Project` 158 | 4. Locate the project directory and click `OK` 159 | 5. Select `Create project from existing sources` and click `Next` 160 | 6. Rename the project if you want. Click `Next` 161 | 7. Ensure that your src folder is checked. Keep clicking `Next` 162 | 8. Click `Finish` 163 | 164 | ## Design 165 | 166 | AddressBook saves data in a plain text file, one line for each person, in the format `NAME p/PHONE e/EMAIL`. 167 | Here is an example: 168 | 169 | ``` 170 | John Doe p/98765432 e/johnd@gmail.com 171 | Jane Doe p/12346758 e/jane@gmail.com 172 | ``` 173 | 174 | All person data are loaded to memory at start up and written to the file after any command that mutates data. 175 | In-memory data are held in a `ArrayList` where each `String[]` object represents a person. 176 | 177 | 178 | ## Testing 179 | 180 | **Windows** 181 | 182 | 1. Open a DOS window in the `test` folder 183 | 2. Run the `runtests.bat` script 184 | 3. If the script reports that there is no difference between `actual.txt` and `expected.txt`, 185 | the test has passed. 186 | 187 | **Mac/Unix/Linux** 188 | 189 | 1. Open a terminal window in the `test` folder 190 | 2. Run the `runtests.sh` script 191 | 3. If the script reports that there is no difference between `actual.txt` and `expected.txt`, 192 | the test has passed. 193 | 194 | **Troubleshooting test failures** 195 | 196 | * Problem: How do I examine the exact differences between `actual.txt` and `expected.txt`?
197 | Solution: You can use a diff/merge tool with a GUI e.g. WinMerge (on Windows) 198 | * Problem: The two files look exactly the same, but the test script reports they are different.
199 | Solution: This can happen because the line endings used by Windows is different from Unix-based 200 | OSes. Convert the `actual.txt` to the format used by your OS using some [utility](https://kb.iu.edu/d/acux). 201 | * Problem: Test fails during the very first time.
202 | Solution: The output of the very first test run could be slightly different because the program 203 | creates a new storage file. Tests should pass from the 2nd run onwards. 204 | 205 | ----------------------------------------------------------------------------------------------------- 206 | # Learning Outcomes 207 | _Learning Outcomes_ are the things you should be able to do after studying this code and completing the 208 | corresponding exercises. 209 | 210 | ## Set up a project in an IDE `[LO-IdeSetup]` 211 | 212 | * Learn [how to set up a project in IntelliJ](https://se-edu.github.io/se-book/intellij/projectSetup/). 213 | 214 | #### Exercise: Setup project in IntelliJ 215 | 216 | Part A: 217 | * Create a new project in IntelliJ and write a small HelloWorld program. 218 | 219 | Part B: 220 | * Download the source code for this project: Click on the `clone or download` link above and either, 221 | 1. download as a zip file and unzip content. 222 | 2. clone the repo (if you know how to use Git) to your Computer. 223 | * [Set up](#setting-up) the project in IntelliJ. 224 | * [Run the program](#starting-the-program) from within IntelliJ, and try the features described in 225 | the [User guide](#user-guide) section. 226 | 227 | ## Navigate code efficiently `[LO-CodeNavigation]` 228 | 229 | The `AddressBook.java` code is rather long, which makes it cumbersome to navigate by scrolling alone. 230 | Navigating code using IDE shortcuts is a more efficient option. 231 | For example, CTRL+B will navigate to the definition of the method/field at the cursor. 232 | 233 | Learn [basic IntelliJ code navigation features](https://se-edu.github.io/se-book/intellij/codeNavigation/). 234 | 235 | #### Exercise: Learn to navigate code using shortcuts 236 | 237 | * Use Intellij basic code navigation features to navigate code of this project. 238 | 239 | ## Use a debugger `[LO-Debugging]` 240 | 241 | Learn [basic IntelliJ debugging features](https://se-edu.github.io/se-book/intellij/debuggingBasic/). 242 | 243 | #### Exercise: Learn to step through code using the debugger 244 | 245 | Prerequisite: `[LO-IdeSetup]` 246 | 247 | Demonstrate your debugging skills using the AddressBook code. 248 | 249 | Here are some things you can do in your demonstration: 250 | 251 | 1. Set a 'break point' 252 | 2. Run the program in debug mode 253 | 3. 'Step through' a few lines of code while examining variable values 254 | 4. 'Step into', and 'step out of', methods as you step through the code 255 | 5. ... 256 | 257 | ## Automate CLI testing `[LO-AutomatedCliTesting]` 258 | 259 | Learn [how to automate testing of CLIs](https://se-edu.github.io/se-book/testing/testAutomation/testingTextUis/). 260 | 261 | #### Exercise: Practice automated CLI testing 262 | 263 | * Run the tests as explained in the [Testing](#testing) section. 264 | * Examine the test script to understand how the script works. 265 | * Add a few more tests to the `input.txt`. Run the tests. It should fail.
266 | Modify `expected.txt` to make the tests pass again. 267 | * Edit the `AddressBook.java` to modify the behavior slightly and modify tests to match. 268 | 269 | ## Use Collections `[LO-Collections]` 270 | 271 | Note how the `AddressBook` class uses `ArrayList<>` class (from the Java `Collections` library) to store a list of `String` or `String[]` objects. 272 | 273 | Learn [how to use some Java `Collections` classes, such as `ArrayList` and `HashMap`](https://se-edu.github.io/se-book/javaTools/collections/) 274 | 275 | 276 | #### Exercise: Use `HashMap` 277 | 278 | Currently, a person's details are stored as a `String[]`. Modify the code to use a `HashMap` instead. 279 | A sample code snippet is given below. 280 | 281 | ```java 282 | private static final String PERSON_PROPERTY_NAME = "name"; 283 | private static final String PERSON_PROPERTY_EMAIL = "email"; 284 | ... 285 | HashMap john = new HashMap<>(); 286 | john.put(PERSON_PROPERTY_NAME, "John Doe"); 287 | john.put(PERSON_PROPERTY_EMAIL, "john.doe@email.com"); 288 | //etc. 289 | ``` 290 | 291 | ## Use Enums `[LO-Enums]` 292 | 293 | #### Exercise: Use `HashMap` + `Enum` 294 | 295 | Similar to the exercise in the `LO-Collections` section, but also bring in Java `enum` feature. 296 | 297 | ```java 298 | private enum PersonProperty {NAME, EMAIL, PHONE}; 299 | ... 300 | HashMap john = new HashMap<>(); 301 | john.put(PersonProperty.NAME, "John Doe"); 302 | john.put(PersonProperty.EMAIL, "john.doe@email.com"); 303 | //etc. 304 | ``` 305 | 306 | ## Use Varargs `[LO-Varargs]` 307 | 308 | Note how the `showToUser` method uses [Java Varargs feature](https://se-edu.github.io/se-book/javaTools/varargs/). 309 | 310 | #### Exercise: Use Varargs 311 | 312 | Modify the code to remove the use of the Varargs feature. 313 | Compare the code with and without the varargs feature. 314 | 315 | ## Follow a coding standard `[LO-CodingStandard]` 316 | 317 | The given code follows the [coding standard][coding-standard] 318 | for the most part. 319 | 320 | This learning outcome is covered by the exercise in `[LO-Refactor]`. 321 | 322 | ## Apply coding best practices `[LO-CodingBestPractices]` 323 | 324 | Most of the given code follows the best practices mentioned 325 | [here][code-quality]. 326 | 327 | This learning outcome is covered by the exercise in `[LO-Refactor]` 328 | 329 | ## Refactor code `[LO-Refactor]` 330 | 331 | **Resources**: 332 | 333 | * [se-edu/se-gook: Refactoring](https://se-edu.github.io/se-book/refactoring/) 334 | * [se-edu/se-book: Refactoring in Intellij](https://se-edu.github.io/se-book/intellij/refactoring/) 335 | 336 | #### Exercise: Refactor the code to make it better 337 | 338 | Note: this exercise covers two other Learning Outcomes: `[LO-CodingStandard]`, `[LO-CodingBestPractices]` 339 | 340 | * Improve the code in the following ways, 341 | * Fix [coding standard][coding-standard] 342 | violations. 343 | * Fix violations of the best practices given in [in this document][code-quality]. 344 | * Any other change that you think will improve the quality of the code. 345 | * Try to do the modifications as a combination of standard refactorings given in this 346 | [catalog](http://refactoring.com/catalog/) 347 | * As far as possible, use automated refactoring features in IntelliJ. 348 | * If you know how to use Git, commit code after each refactoring.
349 | In the commit message, mention which refactoring you applied.
350 | Example commit messages: `Extract variable isValidPerson`, `Inline method isValidPerson()` 351 | * Remember to run the test script after each refactoring to prevent [regressions](https://se-edu.github.io/se-book/testing/testingTypes/regressionTesting). 352 | 353 | ## Abstract methods well `[LO-MethodAbstraction]` 354 | 355 | Notice how most of the methods in `AddressBook` are short and focused (does only one thing and does it well). 356 | 357 | **Case 1**. Consider the following three lines in the `main` method. 358 | 359 | ```java 360 | String userCommand = getUserInput(); 361 | echoUserCommand(userCommand); 362 | String feedback = executeCommand(userCommand); 363 | ``` 364 | 365 | If we include the code of `echoUserCommand(String)` method inside the `getUserInput()` 366 | (resulting in the code given below), the behavior of AddressBook remains as before. 367 | However, that is a not a good approach because now the `getUserInput()` is doing two distinct things. 368 | A well-abstracted method should do only one thing. 369 | 370 | ```java 371 | String userCommand = getUserInput(); //also echos the command back to the user 372 | String feedback = executeCommand(userCommand); 373 | ``` 374 | 375 | **Case 2**. Consider the method `removePrefixSign(String s, String sign)`. 376 | While it is short, there are some problems with how it has been abstracted. 377 | 378 | 1. It contains the term `sign` which is not a term used by the AddressBook vocabulary. 379 | 380 | > **A method adds a new term to the vocabulary used to express the solution**. 381 | > Therefore, it is not good when a method name contains terms that are not strictly necessary to express the 382 | > solution (e.g. there is another term already used to express the same thing) or not in tune with the solution 383 | > (e.g. it does not go well with the other terms already used). 384 | 385 | 2. Its implementation is not doing exactly what is advertised by the method name and the header comment. 386 | For example, the code does not remove only prefixes; it removes `sign` from anywhere in the `s`. 387 | 3. The method can be _more general_ and _more independent_ from the rest of the code. For example, 388 | the method below can do the same job, but is more general (works for any string, not just parameters) 389 | and is more independent from the rest of the code (not specific to AddressBook) 390 | 391 | ```java 392 | /** 393 | * Removes prefix from the given fullString if prefix occurs at the start of the string. 394 | */ 395 | private static String removePrefix(String fullString, String prefix) { ... } 396 | ``` 397 | 398 | If needed, a more AddressBook-specific method that works on parameter strings only can be defined. 399 | In that case, that method can make use of the more general method suggested above. 400 | 401 | #### Exercise: Improve abstraction of method 402 | 403 | Refactor the method `removePrefixSign` as suggested above. 404 | 405 | 406 | ## Follow SLAP `[LO-SLAP]` 407 | 408 | Notice how most of the methods in `AddressBook` are written at a single 409 | level of abstraction (_cf_ [se-edu/se-book:SLAP](https://se-edu.github.io/se-book/codeQuality/practices/slapHard/)) 410 | 411 | Here is an example: 412 | 413 | ```java 414 | public static void main(String[] args) { 415 | showWelcomeMessage(); 416 | processProgramArgs(args); 417 | loadDataFromStorage(); 418 | while (true) { 419 | userCommand = getUserInput(); 420 | echoUserCommand(userCommand); 421 | String feedback = executeCommand(userCommand); 422 | showResultToUser(feedback); 423 | } 424 | } 425 | ``` 426 | 427 | #### Exercise 1: Reduce SLAP of method 428 | 429 | In the `main` method, replace the `processProgramArgs(args)` call with the actual code of that method. 430 | The `main` method no longer has SLAP. Notice how mixing low level code with high level code reduces 431 | readability. 432 | 433 | #### Exercise 2: Refactor the code to make it worse! 434 | 435 | Sometimes, going in the wrong direction can be a good learning experience too. 436 | In this exercise, we explore how low code qualities can go. 437 | 438 | * Refactor the code to make the code as bad as possible.
439 | i.e. How bad can you make it without breaking the functionality while still making it look like it was written by a 440 | programmer (but a very bad programmer :-)). 441 | * In particular, inlining methods can worsen the code quality fast. 442 | 443 | ## Work in a 1kLoC code base`[LO-1KLoC]` 444 | 445 | #### Exercise: Enhance the code 446 | 447 | Enhance the AddressBook to prove that you can work in a codebase of 1KLoC.
448 | Remember to change code in small steps, update/run tests after each change, and commit after each significant change. 449 | 450 | Some suggested enhancements: 451 | 452 | * Make the `find` command case insensitive e.g. `find john` should match `John` 453 | * Add a `sort` command that can list the persons in alphabetical order 454 | * Add an `edit` command that can edit properties of a specific person 455 | * Add an additional field (like date of birth) to the person record 456 | 457 | ----------------------------------------------------------------------------------------------------- 458 | # Contributors 459 | 460 | The full list of contributors for se-edu can be found [here](https://se-edu.github.io/docs/Team.html). 461 | 462 | ----------------------------------------------------------------------------------------------------- 463 | # Contact Us 464 | 465 | * **Bug reports, Suggestions**: Post in our [issue tracker](https://github.com/se-edu/addressbook-level1/issues) 466 | if you noticed bugs or have suggestions on how to improve. 467 | * **Contributing**: We welcome pull requests. Refer to our website [here](https://se-edu.github.io/#contributing). 468 | * If you would like to contact us, refer to [our website](https://se-edu.github.io/#contact). 469 | 470 | [coding-standard]: https://github.com/oss-generic/process/blob/master/codingStandards/CodingStandard-Java.md "Java Coding Standard" 471 | [code-quality]: https://se-edu.github.io/se-book/codeQuality/ "Code Quality Best Practices" -------------------------------------------------------------------------------- /src/seedu/addressbook/AddressBook.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook; 2 | 3 | /* 4 | * NOTE : ============================================================= 5 | * This class is written in a procedural fashion (i.e. not Object-Oriented) 6 | * Yes, it is possible to write non-OO code using an OO language. 7 | * ==================================================================== 8 | */ 9 | 10 | import java.io.File; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.InvalidPathException; 15 | import java.nio.file.Path; 16 | import java.nio.file.Paths; 17 | import java.util.*; 18 | 19 | /* 20 | * NOTE : ============================================================= 21 | * This class header comment below is brief because details of how to 22 | * use this class are documented elsewhere. 23 | * ==================================================================== 24 | */ 25 | 26 | /** 27 | * This class is used to maintain a list of person data which are saved 28 | * in a text file. 29 | **/ 30 | public class AddressBook { 31 | 32 | /** 33 | * Default file path used if the user doesn't provide the file name. 34 | */ 35 | private static final String DEFAULT_STORAGE_FILEPATH = "addressbook.txt"; 36 | 37 | /** 38 | * Version info of the program. 39 | */ 40 | private static final String VERSION = "AddessBook Level 1 - Version 1.0"; 41 | 42 | /** 43 | * A decorative prefix added to the beginning of lines printed by AddressBook 44 | */ 45 | private static final String LINE_PREFIX = "][ "; 46 | 47 | /** 48 | * A platform independent line separator. 49 | */ 50 | private static final String LS = System.lineSeparator() + LINE_PREFIX; 51 | 52 | /* 53 | * NOTE : ================================================================== 54 | * These messages shown to the user are defined in one place for convenient 55 | * editing and proof reading. Such messages are considered part of the UI 56 | * and may be subjected to review by UI experts or technical writers. Note 57 | * that Some of the strings below include '%1$s' etc to mark the locations 58 | * at which java String.format(...) method can insert values. 59 | * ========================================================================= 60 | */ 61 | private static final String MESSAGE_ADDED = "New person added: %1$s, Phone: %2$s, Email: %3$s"; 62 | private static final String MESSAGE_ADDRESSBOOK_CLEARED = "Address book has been cleared!"; 63 | private static final String MESSAGE_COMMAND_HELP = "%1$s: %2$s"; 64 | private static final String MESSAGE_COMMAND_HELP_PARAMETERS = "\tParameters: %1$s"; 65 | private static final String MESSAGE_COMMAND_HELP_EXAMPLE = "\tExample: %1$s"; 66 | private static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; 67 | private static final String MESSAGE_DISPLAY_PERSON_DATA = "%1$s Phone Number: %2$s Email: %3$s"; 68 | private static final String MESSAGE_DISPLAY_LIST_ELEMENT_INDEX = "%1$d. "; 69 | private static final String MESSAGE_GOODBYE = "Exiting Address Book... Good bye!"; 70 | private static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format: %1$s " + LS + "%2$s"; 71 | private static final String MESSAGE_INVALID_FILE = "The given file name [%1$s] is not a valid file name!"; 72 | private static final String MESSAGE_INVALID_PROGRAM_ARGS = "Too many parameters! Correct program argument format:" 73 | + LS + "\tjava AddressBook" 74 | + LS + "\tjava AddressBook [custom storage file path]"; 75 | private static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; 76 | private static final String MESSAGE_INVALID_STORAGE_FILE_CONTENT = "Storage file has invalid content"; 77 | private static final String MESSAGE_PERSON_NOT_IN_ADDRESSBOOK = "Person could not be found in address book"; 78 | private static final String MESSAGE_ERROR_CREATING_STORAGE_FILE = "Error: unable to create file: %1$s"; 79 | private static final String MESSAGE_ERROR_MISSING_STORAGE_FILE = "Storage file missing: %1$s"; 80 | private static final String MESSAGE_ERROR_READING_FROM_FILE = "Unexpected error: unable to read from file: %1$s"; 81 | private static final String MESSAGE_ERROR_WRITING_TO_FILE = "Unexpected error: unable to write to file: %1$s"; 82 | private static final String MESSAGE_PERSONS_FOUND_OVERVIEW = "%1$d persons found!"; 83 | private static final String MESSAGE_STORAGE_FILE_CREATED = "Created new empty storage file: %1$s"; 84 | private static final String MESSAGE_WELCOME = "Welcome to your Address Book!"; 85 | private static final String MESSAGE_USING_DEFAULT_FILE = "Using default storage file : " + DEFAULT_STORAGE_FILEPATH; 86 | 87 | // These are the prefix strings to define the data type of a command parameter 88 | private static final String PERSON_DATA_PREFIX_PHONE = "p/"; 89 | private static final String PERSON_DATA_PREFIX_EMAIL = "e/"; 90 | 91 | private static final String PERSON_STRING_REPRESENTATION = "%1$s " // name 92 | + PERSON_DATA_PREFIX_PHONE + "%2$s " // phone 93 | + PERSON_DATA_PREFIX_EMAIL + "%3$s"; // email 94 | private static final String COMMAND_ADD_WORD = "add"; 95 | private static final String COMMAND_ADD_DESC = "Adds a person to the address book."; 96 | private static final String COMMAND_ADD_PARAMETERS = "NAME " 97 | + PERSON_DATA_PREFIX_PHONE + "PHONE_NUMBER " 98 | + PERSON_DATA_PREFIX_EMAIL + "EMAIL"; 99 | private static final String COMMAND_ADD_EXAMPLE = COMMAND_ADD_WORD + " John Doe p/98765432 e/johnd@gmail.com"; 100 | 101 | private static final String COMMAND_FIND_WORD = "find"; 102 | private static final String COMMAND_FIND_DESC = "Finds all persons whose names contain any of the specified " 103 | + "keywords (case-insensitive) and displays them as a list with index numbers."; 104 | private static final String COMMAND_FIND_PARAMETERS = "KEYWORD [MORE_KEYWORDS]"; 105 | private static final String COMMAND_FIND_EXAMPLE = COMMAND_FIND_WORD + " alice bob charlie"; 106 | 107 | private static final String COMMAND_LIST_WORD = "list"; 108 | private static final String COMMAND_LIST_DESC = "Displays all persons as a list with index numbers."; 109 | private static final String COMMAND_LIST_EXAMPLE = COMMAND_LIST_WORD; 110 | 111 | private static final String COMMAND_DELETE_WORD = "delete"; 112 | private static final String COMMAND_DELETE_DESC = "Deletes a person identified by the index number used in " 113 | + "the last find/list call."; 114 | private static final String COMMAND_DELETE_PARAMETER = "INDEX"; 115 | private static final String COMMAND_DELETE_EXAMPLE = COMMAND_DELETE_WORD + " 1"; 116 | 117 | private static final String COMMAND_CLEAR_WORD = "clear"; 118 | private static final String COMMAND_CLEAR_DESC = "Clears address book permanently."; 119 | private static final String COMMAND_CLEAR_EXAMPLE = COMMAND_CLEAR_WORD; 120 | 121 | private static final String COMMAND_HELP_WORD = "help"; 122 | private static final String COMMAND_HELP_DESC = "Shows program usage instructions."; 123 | private static final String COMMAND_HELP_EXAMPLE = COMMAND_HELP_WORD; 124 | 125 | private static final String COMMAND_EXIT_WORD = "exit"; 126 | private static final String COMMAND_EXIT_DESC = "Exits the program."; 127 | private static final String COMMAND_EXIT_EXAMPLE = COMMAND_EXIT_WORD; 128 | 129 | private static final String DIVIDER = "==================================================="; 130 | 131 | 132 | /* We use a HashMap to store details of a single person. 133 | * The enum type given below is the keys for different data elements of a person 134 | * used by the internal HashMap storage format. 135 | * For example, a person's name is stored as a key-value pair in a HashMap with key PersonProperty.NAME 136 | */ 137 | private enum PersonProperty { 138 | NAME, EMAIL, PHONE 139 | } 140 | 141 | /** 142 | * The number of data elements for a single person. 143 | */ 144 | private static final int PERSON_DATA_COUNT = 3; 145 | 146 | /** 147 | * Offset required to convert between 1-indexing and 0-indexing.COMMAND_ 148 | */ 149 | private static final int DISPLAYED_INDEX_OFFSET = 1; 150 | 151 | /** 152 | * If the first non-whitespace character in a user's input line is this, that line will be ignored. 153 | */ 154 | private static final char INPUT_COMMENT_MARKER = '#'; 155 | 156 | /* 157 | * This variable is declared for the whole class (instead of declaring it 158 | * inside the readUserCommand() method to facilitate automated testing using 159 | * the I/O redirection technique. If not, only the first line of the input 160 | * text file will be processed. 161 | */ 162 | private static final Scanner SCANNER = new Scanner(System.in); 163 | 164 | /* 165 | * NOTE : ============================================================================================= 166 | * Note that the type of the variable below can also be declared as List>, as follows: 167 | * private static final List> ALL_PERSONS = new ArrayList<>() 168 | * That is because List is an interface implemented by the ArrayList class. 169 | * In this code we use ArrayList instead because we wanted to to stay away from advanced concepts 170 | * such as interface inheritance. 171 | * ==================================================================================================== 172 | */ 173 | 174 | /** 175 | * List of all persons in the address book. 176 | */ 177 | private static final ArrayList> ALL_PERSONS = new ArrayList<>(); 178 | 179 | /** 180 | * Stores the most recent list of persons shown to the user as a result of a user command. 181 | * This is a subset of the full list. Deleting persons in the pull list does not delete 182 | * those persons from this list. 183 | */ 184 | private static ArrayList> latestPersonListingView = getAllPersonsInAddressBook(); // initial view is of all 185 | 186 | /** 187 | * The path to the file used for storing person data. 188 | */ 189 | private static String storageFilePath; 190 | 191 | /* 192 | * NOTE : ============================================================= 193 | * Notice how this method solves the whole problem at a very high level. 194 | * We can understand the high-level logic of the program by reading this 195 | * method alone. 196 | * If the reader wants a deeper understanding of the solution, she can go 197 | * to the next level of abstraction by reading the methods that are 198 | * referenced by the high-level method below. 199 | * ==================================================================== 200 | */ 201 | 202 | public static void main(String[] args) { 203 | showWelcomeMessage(); 204 | processProgramArgs(args); 205 | loadDataFromStorage(); 206 | while (true) { 207 | String userCommand = getUserInput(); 208 | echoUserCommand(userCommand); 209 | String feedback = executeCommand(userCommand); 210 | showResultToUser(feedback); 211 | } 212 | } 213 | 214 | /* 215 | * NOTE : ============================================================= 216 | * The method header comment can be omitted if the method is trivial 217 | * and the header comment is going to be almost identical to the method 218 | * signature anyway. 219 | * ==================================================================== 220 | */ 221 | 222 | private static void showWelcomeMessage() { 223 | showToUser(DIVIDER, DIVIDER, VERSION, MESSAGE_WELCOME, DIVIDER); 224 | } 225 | 226 | private static void showResultToUser(String result) { 227 | showToUser(result, DIVIDER); 228 | } 229 | 230 | /* 231 | * NOTE : ============================================================= 232 | * Parameter description can be omitted from the method header comment 233 | * if the parameter name is self-explanatory. 234 | * In the method below, '@param userInput' comment has been omitted. 235 | * ==================================================================== 236 | */ 237 | 238 | /** 239 | * Echoes the user input back to the user. 240 | */ 241 | private static void echoUserCommand(String userCommand) { 242 | showToUser("[Command entered:" + userCommand + "]"); 243 | } 244 | 245 | /** 246 | * Processes the program main method run arguments. 247 | * If a valid storage file is specified, sets up that file for storage. 248 | * Otherwise sets up the default file for storage. 249 | * 250 | * @param args full program arguments passed to application main method 251 | */ 252 | private static void processProgramArgs(String[] args) { 253 | if (args.length >= 2) { 254 | showToUser(MESSAGE_INVALID_PROGRAM_ARGS); 255 | exitProgram(); 256 | } 257 | 258 | if (args.length == 1) { 259 | setupGivenFileForStorage(args[0]); 260 | } 261 | 262 | if (args.length == 0) { 263 | setupDefaultFileForStorage(); 264 | } 265 | } 266 | 267 | /** 268 | * Sets up the storage file based on the supplied file path. 269 | * Creates the file if it is missing. 270 | * Exits if the file name is not acceptable. 271 | */ 272 | private static void setupGivenFileForStorage(String filePath) { 273 | 274 | if (!isValidFilePath(filePath)) { 275 | showToUser(String.format(MESSAGE_INVALID_FILE, filePath)); 276 | exitProgram(); 277 | } 278 | 279 | storageFilePath = filePath; 280 | createFileIfMissing(filePath); 281 | } 282 | 283 | /** 284 | * Displays the goodbye message and exits the runtime. 285 | */ 286 | private static void exitProgram() { 287 | showToUser(MESSAGE_GOODBYE, DIVIDER, DIVIDER); 288 | System.exit(0); 289 | } 290 | 291 | /** 292 | * Sets up the storage based on the default file. 293 | * Creates file if missing. 294 | * Exits program if the file cannot be created. 295 | */ 296 | private static void setupDefaultFileForStorage() { 297 | showToUser(MESSAGE_USING_DEFAULT_FILE); 298 | storageFilePath = DEFAULT_STORAGE_FILEPATH; 299 | createFileIfMissing(storageFilePath); 300 | } 301 | 302 | /** 303 | * Returns true if the given file path is valid. 304 | * A file path is valid if it has a valid parent directory as determined by {@link #hasValidParentDirectory} 305 | * and a valid file name as determined by {@link #hasValidFileName}. 306 | */ 307 | private static boolean isValidFilePath(String filePath) { 308 | if (filePath == null) { 309 | return false; 310 | } 311 | Path filePathToValidate; 312 | try { 313 | filePathToValidate = Paths.get(filePath); 314 | } catch (InvalidPathException ipe) { 315 | return false; 316 | } 317 | return hasValidParentDirectory(filePathToValidate) && hasValidFileName(filePathToValidate); 318 | } 319 | 320 | /** 321 | * Returns true if the file path has a parent directory that exists. 322 | */ 323 | private static boolean hasValidParentDirectory(Path filePath) { 324 | Path parentDirectory = filePath.getParent(); 325 | return parentDirectory == null || Files.isDirectory(parentDirectory); 326 | } 327 | 328 | /** 329 | * Returns true if file path has a valid file name. 330 | * File name is valid if it has an extension and no reserved characters. 331 | * Reserved characters are OS-dependent. 332 | * If a file already exists, it must be a regular file. 333 | */ 334 | private static boolean hasValidFileName(Path filePath) { 335 | return filePath.getFileName().toString().lastIndexOf('.') > 0 336 | && (!Files.exists(filePath) || Files.isRegularFile(filePath)); 337 | } 338 | 339 | /** 340 | * Initialises the in-memory data using the storage file. 341 | * Assumption: The file exists. 342 | */ 343 | private static void loadDataFromStorage() { 344 | initialiseAddressBookModel(loadPersonsFromFile(storageFilePath)); 345 | } 346 | 347 | 348 | /* 349 | * =========================================== 350 | * COMMAND LOGIC 351 | * =========================================== 352 | */ 353 | 354 | /** 355 | * Executes the command as specified by the {@code userInputString} 356 | * 357 | * @param userInputString raw input from user 358 | * @return feedback about how the command was executed 359 | */ 360 | private static String executeCommand(String userInputString) { 361 | final String[] commandTypeAndParams = splitCommandWordAndArgs(userInputString); 362 | final String commandType = commandTypeAndParams[0]; 363 | final String commandArgs = commandTypeAndParams[1]; 364 | switch (commandType) { 365 | case COMMAND_ADD_WORD: 366 | return executeAddPerson(commandArgs); 367 | case COMMAND_FIND_WORD: 368 | return executeFindPersons(commandArgs); 369 | case COMMAND_LIST_WORD: 370 | return executeListAllPersonsInAddressBook(); 371 | case COMMAND_DELETE_WORD: 372 | return executeDeletePerson(commandArgs); 373 | case COMMAND_CLEAR_WORD: 374 | return executeClearAddressBook(); 375 | case COMMAND_HELP_WORD: 376 | return getUsageInfoForAllCommands(); 377 | case COMMAND_EXIT_WORD: 378 | executeExitProgramRequest(); 379 | default: 380 | return getMessageForInvalidCommandInput(commandType, getUsageInfoForAllCommands()); 381 | } 382 | } 383 | 384 | /** 385 | * Splits raw user input into command word and command arguments string 386 | * 387 | * @return size 2 array; first element is the command type and second element is the arguments string 388 | */ 389 | private static String[] splitCommandWordAndArgs(String rawUserInput) { 390 | final String[] split = rawUserInput.trim().split("\\s+", 2); 391 | return split.length == 2 ? split : new String[]{split[0], ""}; // else case: no parameters 392 | } 393 | 394 | /** 395 | * Constructs a generic feedback message for an invalid command from user, with instructions for correct usage. 396 | * 397 | * @param correctUsageInfo message showing the correct usage 398 | * @return invalid command args feedback message 399 | */ 400 | private static String getMessageForInvalidCommandInput(String userCommand, String correctUsageInfo) { 401 | return String.format(MESSAGE_INVALID_COMMAND_FORMAT, userCommand, correctUsageInfo); 402 | } 403 | 404 | /** 405 | * Adds a person (specified by the command args) to the address book. 406 | * The entire command arguments string is treated as a string representation of the person to add. 407 | * 408 | * @param commandArgs full command args string from the user 409 | * @return feedback display message for the operation result 410 | */ 411 | private static String executeAddPerson(String commandArgs) { 412 | // try decoding a person from the raw args 413 | final Optional> decodeResult = decodePersonFromString(commandArgs); 414 | 415 | // checks if args are valid (decode result will not be present if the person is invalid) 416 | if (!decodeResult.isPresent()) { 417 | return getMessageForInvalidCommandInput(COMMAND_ADD_WORD, getUsageInfoForAddCommand()); 418 | } 419 | 420 | // add the person as specified 421 | final HashMap personToAdd = decodeResult.get(); 422 | addPersonToAddressBook(personToAdd); 423 | return getMessageForSuccessfulAddPerson(personToAdd); 424 | } 425 | 426 | /** 427 | * Constructs a feedback message for a successful add person command execution. 428 | * 429 | * @param addedPerson person who was successfully added 430 | * @return successful add person feedback message 431 | * @see #executeAddPerson(String) 432 | */ 433 | private static String getMessageForSuccessfulAddPerson(HashMap addedPerson) { 434 | return String.format(MESSAGE_ADDED, 435 | getNameFromPerson(addedPerson), getPhoneFromPerson(addedPerson), getEmailFromPerson(addedPerson)); 436 | } 437 | 438 | /** 439 | * Finds and lists all persons in address book whose name contains any of the argument keywords. 440 | * Keyword matching is case sensitive. 441 | * 442 | * @param commandArgs full command args string from the user 443 | * @return feedback display message for the operation result 444 | */ 445 | private static String executeFindPersons(String commandArgs) { 446 | final Set keywords = extractKeywordsFromFindPersonArgs(commandArgs); 447 | final ArrayList> personsFound = getPersonsWithNameContainingAnyKeyword(keywords); 448 | showToUser(personsFound); 449 | return getMessageForPersonsDisplayedSummary(personsFound); 450 | } 451 | 452 | /** 453 | * Constructs a feedback message to summarise an operation that displayed a listing of persons. 454 | * 455 | * @param personsDisplayed used to generate summary 456 | * @return summary message for persons displayed 457 | */ 458 | private static String getMessageForPersonsDisplayedSummary(ArrayList> personsDisplayed) { 459 | return String.format(MESSAGE_PERSONS_FOUND_OVERVIEW, personsDisplayed.size()); 460 | } 461 | 462 | /** 463 | * Extracts keywords from the command arguments given for the find persons command. 464 | * 465 | * @param findPersonCommandArgs full command args string for the find persons command 466 | * @return set of keywords as specified by args 467 | */ 468 | private static Set extractKeywordsFromFindPersonArgs(String findPersonCommandArgs) { 469 | return new HashSet<>(splitByWhitespace(findPersonCommandArgs.trim())); 470 | } 471 | 472 | /** 473 | * Retrieves all persons in the full model whose names contain some of the specified keywords. 474 | * 475 | * @param keywords for searching 476 | * @return list of persons in full model with name containing some of the keywords 477 | */ 478 | private static ArrayList> getPersonsWithNameContainingAnyKeyword(Collection keywords) { 479 | Set keywordsInLowerCase = convertToLowerCase(keywords); 480 | final ArrayList> matchedPersons = new ArrayList<>(); 481 | for (HashMap person : getAllPersonsInAddressBook()) { 482 | final Set wordsInName = new HashSet<>(splitByWhitespace(getNameFromPerson(person))); 483 | Set wordsInNameInLowerCase = convertToLowerCase(wordsInName); 484 | if (!Collections.disjoint(wordsInNameInLowerCase, keywordsInLowerCase)) { 485 | matchedPersons.add(person); 486 | } 487 | } 488 | return matchedPersons; 489 | } 490 | 491 | /** 492 | * Deletes person identified using last displayed index. 493 | * 494 | * @param commandArgs full command args string from the user 495 | * @return feedback display message for the operation result 496 | */ 497 | private static String executeDeletePerson(String commandArgs) { 498 | if (!isDeletePersonArgsValid(commandArgs)) { 499 | return getMessageForInvalidCommandInput(COMMAND_DELETE_WORD, getUsageInfoForDeleteCommand()); 500 | } 501 | final int targetVisibleIndex = extractTargetIndexFromDeletePersonArgs(commandArgs); 502 | if (!isDisplayIndexValidForLastPersonListingView(targetVisibleIndex)) { 503 | return MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; 504 | } 505 | final HashMap targetInModel = getPersonByLastVisibleIndex(targetVisibleIndex); 506 | return deletePersonFromAddressBook(targetInModel) ? getMessageForSuccessfulDelete(targetInModel) // success 507 | : MESSAGE_PERSON_NOT_IN_ADDRESSBOOK; // not found 508 | } 509 | 510 | /** 511 | * Checks validity of delete person argument string's format. 512 | * 513 | * @param rawArgs raw command args string for the delete person command 514 | * @return whether the input args string is valid 515 | */ 516 | private static boolean isDeletePersonArgsValid(String rawArgs) { 517 | try { 518 | final int extractedIndex = Integer.parseInt(rawArgs.trim()); // use standard libraries to parse 519 | return extractedIndex >= DISPLAYED_INDEX_OFFSET; 520 | } catch (NumberFormatException nfe) { 521 | return false; 522 | } 523 | } 524 | 525 | /** 526 | * Extracts the target's index from the raw delete person args string 527 | * 528 | * @param rawArgs raw command args string for the delete person command 529 | * @return extracted index 530 | */ 531 | private static int extractTargetIndexFromDeletePersonArgs(String rawArgs) { 532 | return Integer.parseInt(rawArgs.trim()); 533 | } 534 | 535 | /** 536 | * Checks that the given index is within bounds and valid for the last shown person list view. 537 | * 538 | * @param index to check 539 | * @return whether it is valid 540 | */ 541 | private static boolean isDisplayIndexValidForLastPersonListingView(int index) { 542 | return index >= DISPLAYED_INDEX_OFFSET && index < latestPersonListingView.size() + DISPLAYED_INDEX_OFFSET; 543 | } 544 | 545 | /** 546 | * Constructs a feedback message for a successful delete person command execution. 547 | * 548 | * @param deletedPerson successfully deleted 549 | * @return successful delete person feedback message 550 | * @see #executeDeletePerson(String) 551 | */ 552 | private static String getMessageForSuccessfulDelete(HashMap deletedPerson) { 553 | return String.format(MESSAGE_DELETE_PERSON_SUCCESS, getMessageForFormattedPersonData(deletedPerson)); 554 | } 555 | 556 | /** 557 | * Clears all persons in the address book. 558 | * 559 | * @return feedback display message for the operation result 560 | */ 561 | private static String executeClearAddressBook() { 562 | clearAddressBook(); 563 | return MESSAGE_ADDRESSBOOK_CLEARED; 564 | } 565 | 566 | /** 567 | * Displays all persons in the address book to the user; in added order. 568 | * 569 | * @return feedback display message for the operation result 570 | */ 571 | private static String executeListAllPersonsInAddressBook() { 572 | ArrayList> toBeDisplayed = getAllPersonsInAddressBook(); 573 | showToUser(toBeDisplayed); 574 | return getMessageForPersonsDisplayedSummary(toBeDisplayed); 575 | } 576 | 577 | /** 578 | * Requests to terminate the program. 579 | */ 580 | private static void executeExitProgramRequest() { 581 | exitProgram(); 582 | } 583 | 584 | /* 585 | * =========================================== 586 | * UI LOGIC 587 | * =========================================== 588 | */ 589 | 590 | /** 591 | * Prompts for the command and reads the text entered by the user. 592 | * Ignores lines with first non-whitespace char equal to {@link #INPUT_COMMENT_MARKER} (considered comments) 593 | * 594 | * @return full line entered by the user 595 | */ 596 | private static String getUserInput() { 597 | System.out.print(LINE_PREFIX + "Enter command: "); 598 | String inputLine = SCANNER.nextLine(); 599 | // silently consume all blank and comment lines 600 | while (inputLine.trim().isEmpty() || inputLine.trim().charAt(0) == INPUT_COMMENT_MARKER) { 601 | inputLine = SCANNER.nextLine(); 602 | } 603 | return inputLine; 604 | } 605 | 606 | /* 607 | * NOTE : ============================================================= 608 | * Note how the method below uses Java 'Varargs' feature so that the 609 | * method can accept a varying number of message parameters. 610 | * ==================================================================== 611 | */ 612 | 613 | /** 614 | * Shows a message to the user 615 | */ 616 | private static void showToUser(String... message) { 617 | for (String m : message) { 618 | System.out.println(LINE_PREFIX + m); 619 | } 620 | } 621 | 622 | /** 623 | * Shows the list of persons to the user. 624 | * The list will be indexed, starting from 1. 625 | */ 626 | private static void showToUser(ArrayList> persons) { 627 | String listAsString = getDisplayString(persons); 628 | showToUser(listAsString); 629 | updateLatestViewedPersonListing(persons); 630 | } 631 | 632 | /** 633 | * Returns the display string representation of the list of persons. 634 | */ 635 | private static String getDisplayString(ArrayList> persons) { 636 | final StringBuilder messageAccumulator = new StringBuilder(); 637 | for (int i = 0; i < persons.size(); i++) { 638 | final HashMap person = persons.get(i); 639 | final int displayIndex = i + DISPLAYED_INDEX_OFFSET; 640 | messageAccumulator.append('\t') 641 | .append(getIndexedPersonListElementMessage(displayIndex, person)) 642 | .append(LS); 643 | } 644 | return messageAccumulator.toString(); 645 | } 646 | 647 | /** 648 | * Constructs a prettified listing element message to represent a person and their data. 649 | * 650 | * @param visibleIndex visible index for this listing 651 | * @param person to show 652 | * @return formatted listing message with index 653 | */ 654 | private static String getIndexedPersonListElementMessage(int visibleIndex, HashMap person) { 655 | return String.format(MESSAGE_DISPLAY_LIST_ELEMENT_INDEX, visibleIndex) + getMessageForFormattedPersonData(person); 656 | } 657 | 658 | /** 659 | * Constructs a prettified string to show the user a person's data. 660 | * 661 | * @param person to show 662 | * @return formatted message showing internal state 663 | */ 664 | private static String getMessageForFormattedPersonData(HashMap person) { 665 | return String.format(MESSAGE_DISPLAY_PERSON_DATA, 666 | getNameFromPerson(person), getPhoneFromPerson(person), getEmailFromPerson(person)); 667 | } 668 | 669 | /** 670 | * Updates the latest person listing view the user has seen. 671 | * 672 | * @param newListing the new listing of persons 673 | */ 674 | private static void updateLatestViewedPersonListing(ArrayList> newListing) { 675 | // clone to insulate from future changes to arg list 676 | latestPersonListingView = new ArrayList<>(newListing); 677 | } 678 | 679 | /** 680 | * Retrieves the person identified by the displayed index from the last shown listing of persons. 681 | * 682 | * @param lastVisibleIndex displayed index from last shown person listing 683 | * @return the actual person object in the last shown person listing 684 | */ 685 | private static HashMap getPersonByLastVisibleIndex(int lastVisibleIndex) { 686 | return latestPersonListingView.get(lastVisibleIndex - DISPLAYED_INDEX_OFFSET); 687 | } 688 | 689 | 690 | /* 691 | * =========================================== 692 | * STORAGE LOGIC 693 | * =========================================== 694 | */ 695 | 696 | /** 697 | * Creates storage file if it does not exist. Shows feedback to user. 698 | * 699 | * @param filePath file to create if not present 700 | */ 701 | private static void createFileIfMissing(String filePath) { 702 | final File storageFile = new File(filePath); 703 | if (storageFile.exists()) { 704 | return; 705 | } 706 | 707 | showToUser(String.format(MESSAGE_ERROR_MISSING_STORAGE_FILE, filePath)); 708 | 709 | try { 710 | storageFile.createNewFile(); 711 | showToUser(String.format(MESSAGE_STORAGE_FILE_CREATED, filePath)); 712 | } catch (IOException ioe) { 713 | showToUser(String.format(MESSAGE_ERROR_CREATING_STORAGE_FILE, filePath)); 714 | exitProgram(); 715 | } 716 | } 717 | 718 | /** 719 | * Converts contents of a file into a list of persons. 720 | * Shows error messages and exits program if any errors in reading or decoding was encountered. 721 | * 722 | * @param filePath file to load from 723 | * @return the list of decoded persons 724 | */ 725 | private static ArrayList> loadPersonsFromFile(String filePath) { 726 | final Optional>> successfullyDecoded = decodePersonsFromStrings(getLinesInFile(filePath)); 727 | if (!successfullyDecoded.isPresent()) { 728 | showToUser(MESSAGE_INVALID_STORAGE_FILE_CONTENT); 729 | exitProgram(); 730 | } 731 | return successfullyDecoded.get(); 732 | } 733 | 734 | /** 735 | * Gets all lines in the specified file as a list of strings. Line separators are removed. 736 | * Shows error messages and exits program if unable to read from file. 737 | */ 738 | private static ArrayList getLinesInFile(String filePath) { 739 | ArrayList lines = null; 740 | try { 741 | lines = new ArrayList<>(Files.readAllLines(Paths.get(filePath))); 742 | } catch (FileNotFoundException fnfe) { 743 | showToUser(String.format(MESSAGE_ERROR_MISSING_STORAGE_FILE, filePath)); 744 | exitProgram(); 745 | } catch (IOException ioe) { 746 | showToUser(String.format(MESSAGE_ERROR_READING_FROM_FILE, filePath)); 747 | exitProgram(); 748 | } 749 | return lines; 750 | } 751 | 752 | /** 753 | * Saves all data to the file. Exits program if there is an error saving to file. 754 | * 755 | * @param filePath file for saving 756 | */ 757 | private static void savePersonsToFile(ArrayList> persons, String filePath) { 758 | final ArrayList linesToWrite = encodePersonsToStrings(persons); 759 | try { 760 | Files.write(Paths.get(storageFilePath), linesToWrite); 761 | } catch (IOException ioe) { 762 | showToUser(String.format(MESSAGE_ERROR_WRITING_TO_FILE, filePath)); 763 | exitProgram(); 764 | } 765 | } 766 | 767 | 768 | /* 769 | * ================================================================================ 770 | * INTERNAL ADDRESS BOOK DATA METHODS 771 | * ================================================================================ 772 | */ 773 | 774 | /** 775 | * Adds a person to the address book. Saves changes to storage file. 776 | * 777 | * @param person to add 778 | */ 779 | private static void addPersonToAddressBook(HashMap person) { 780 | ALL_PERSONS.add(person); 781 | savePersonsToFile(getAllPersonsInAddressBook(), storageFilePath); 782 | } 783 | 784 | /** 785 | * Deletes the specified person from the addressbook if it is inside. Saves any changes to storage file. 786 | * 787 | * @param exactPerson the actual person inside the address book (exactPerson == the person to delete in the full list) 788 | * @return true if the given person was found and deleted in the model 789 | */ 790 | private static boolean deletePersonFromAddressBook(HashMap exactPerson) { 791 | final boolean isChanged = ALL_PERSONS.remove(exactPerson); 792 | if (isChanged) { 793 | savePersonsToFile(getAllPersonsInAddressBook(), storageFilePath); 794 | } 795 | return isChanged; 796 | } 797 | 798 | /** 799 | * Returns all persons in the address book 800 | */ 801 | private static ArrayList> getAllPersonsInAddressBook() { 802 | return ALL_PERSONS; 803 | } 804 | 805 | /** 806 | * Clears all persons in the address book and saves changes to file. 807 | */ 808 | private static void clearAddressBook() { 809 | ALL_PERSONS.clear(); 810 | savePersonsToFile(getAllPersonsInAddressBook(), storageFilePath); 811 | } 812 | 813 | /** 814 | * Resets the internal model with the given data. Does not save to file. 815 | * 816 | * @param persons list of persons to initialise the model with 817 | */ 818 | private static void initialiseAddressBookModel(ArrayList> persons) { 819 | ALL_PERSONS.clear(); 820 | ALL_PERSONS.addAll(persons); 821 | } 822 | 823 | 824 | /* 825 | * =========================================== 826 | * PERSON METHODS 827 | * =========================================== 828 | */ 829 | 830 | /** 831 | * Returns the given person's name 832 | * 833 | * @param person whose name you want 834 | */ 835 | private static String getNameFromPerson(HashMap person) { 836 | return person.get(PersonProperty.NAME); 837 | } 838 | 839 | /** 840 | * Returns given person's phone number 841 | * 842 | * @param person whose phone number you want 843 | */ 844 | private static String getPhoneFromPerson(HashMap person) { 845 | return person.get(PersonProperty.PHONE); 846 | } 847 | 848 | /** 849 | * Returns given person's email 850 | * 851 | * @param person whose email you want 852 | */ 853 | private static String getEmailFromPerson(HashMap person) { 854 | return person.get(PersonProperty.EMAIL); 855 | } 856 | 857 | /** 858 | * Creates a person from the given data. 859 | * 860 | * @param name of person 861 | * @param phone without data prefix 862 | * @param email without data prefix 863 | * @return constructed person 864 | */ 865 | private static HashMap makePersonFromData(String name, String phone, String email) { 866 | final HashMap person = new HashMap(); 867 | person.put(PersonProperty.NAME, name); 868 | person.put(PersonProperty.PHONE, phone); 869 | person.put(PersonProperty.EMAIL, email); 870 | return person; 871 | } 872 | 873 | /** 874 | * Encodes a person into a decodable and readable string representation. 875 | * 876 | * @param person to be encoded 877 | * @return encoded string 878 | */ 879 | private static String encodePersonToString(HashMap person) { 880 | return String.format(PERSON_STRING_REPRESENTATION, 881 | getNameFromPerson(person), getPhoneFromPerson(person), getEmailFromPerson(person)); 882 | } 883 | 884 | /** 885 | * Encodes list of persons into list of decodable and readable string representations. 886 | * 887 | * @param persons to be encoded 888 | * @return encoded strings 889 | */ 890 | private static ArrayList encodePersonsToStrings(ArrayList> persons) { 891 | final ArrayList encoded = new ArrayList<>(); 892 | for (HashMap person : persons) { 893 | encoded.add(encodePersonToString(person)); 894 | } 895 | return encoded; 896 | } 897 | 898 | /* 899 | * NOTE : ============================================================= 900 | * Note the use of Java's new 'Optional' feature to indicate that 901 | * the return value may not always be present. 902 | * ==================================================================== 903 | */ 904 | 905 | /** 906 | * Decodes a person from it's supposed string representation. 907 | * 908 | * @param encoded string to be decoded 909 | * @return if cannot decode: empty Optional 910 | * else: Optional containing decoded person 911 | */ 912 | private static Optional> decodePersonFromString(String encoded) { 913 | // check that we can extract the parts of a person from the encoded string 914 | if (!isPersonDataExtractableFrom(encoded)) { 915 | return Optional.empty(); 916 | } 917 | final HashMap decodedPerson = makePersonFromData( 918 | extractNameFromPersonString(encoded), 919 | extractPhoneFromPersonString(encoded), 920 | extractEmailFromPersonString(encoded) 921 | ); 922 | // check that the constructed person is valid 923 | return isPersonDataValid(decodedPerson) ? Optional.of(decodedPerson) : Optional.empty(); 924 | } 925 | 926 | /** 927 | * Decodes persons from a list of string representations. 928 | * 929 | * @param encodedPersons strings to be decoded 930 | * @return if cannot decode any: empty Optional 931 | * else: Optional containing decoded persons 932 | */ 933 | private static Optional>> decodePersonsFromStrings(ArrayList encodedPersons) { 934 | final ArrayList> decodedPersons = new ArrayList<>(); 935 | for (String encodedPerson : encodedPersons) { 936 | final Optional> decodedPerson = decodePersonFromString(encodedPerson); 937 | if (!decodedPerson.isPresent()) { 938 | return Optional.empty(); 939 | } 940 | decodedPersons.add(decodedPerson.get()); 941 | } 942 | return Optional.of(decodedPersons); 943 | } 944 | 945 | /** 946 | * Returns true if person data (email, name, phone etc) can be extracted from the argument string. 947 | * Format is [name] p/[phone] e/[email], phone and email positions can be swapped. 948 | * 949 | * @param personData person string representation 950 | */ 951 | private static boolean isPersonDataExtractableFrom(String personData) { 952 | final String matchAnyPersonDataPrefix = PERSON_DATA_PREFIX_PHONE + '|' + PERSON_DATA_PREFIX_EMAIL; 953 | final String[] splitArgs = personData.trim().split(matchAnyPersonDataPrefix); 954 | return splitArgs.length == 3 // 3 arguments 955 | && !splitArgs[0].isEmpty() // non-empty arguments 956 | && !splitArgs[1].isEmpty() 957 | && !splitArgs[2].isEmpty(); 958 | } 959 | 960 | /** 961 | * Extracts substring representing person name from person string representation 962 | * 963 | * @param encoded person string representation 964 | * @return name argument 965 | */ 966 | private static String extractNameFromPersonString(String encoded) { 967 | final int indexOfPhonePrefix = encoded.indexOf(PERSON_DATA_PREFIX_PHONE); 968 | final int indexOfEmailPrefix = encoded.indexOf(PERSON_DATA_PREFIX_EMAIL); 969 | // name is leading substring up to first data prefix symbol 970 | int indexOfFirstPrefix = Math.min(indexOfEmailPrefix, indexOfPhonePrefix); 971 | return encoded.substring(0, indexOfFirstPrefix).trim(); 972 | } 973 | 974 | /** 975 | * Extracts substring representing phone number from person string representation 976 | * 977 | * @param encoded person string representation 978 | * @return phone number argument WITHOUT prefix 979 | */ 980 | private static String extractPhoneFromPersonString(String encoded) { 981 | final int indexOfPhonePrefix = encoded.indexOf(PERSON_DATA_PREFIX_PHONE); 982 | final int indexOfEmailPrefix = encoded.indexOf(PERSON_DATA_PREFIX_EMAIL); 983 | 984 | // phone is last arg, target is from prefix to end of string 985 | if (indexOfPhonePrefix > indexOfEmailPrefix) { 986 | return removePrefix(encoded.substring(indexOfPhonePrefix, encoded.length()).trim(), 987 | PERSON_DATA_PREFIX_PHONE); 988 | 989 | // phone is middle arg, target is from own prefix to next prefix 990 | } else { 991 | return removePrefix( 992 | encoded.substring(indexOfPhonePrefix, indexOfEmailPrefix).trim(), 993 | PERSON_DATA_PREFIX_PHONE); 994 | } 995 | } 996 | 997 | /** 998 | * Extracts substring representing email from person string representation 999 | * 1000 | * @param encoded person string representation 1001 | * @return email argument WITHOUT prefix 1002 | */ 1003 | private static String extractEmailFromPersonString(String encoded) { 1004 | final int indexOfPhonePrefix = encoded.indexOf(PERSON_DATA_PREFIX_PHONE); 1005 | final int indexOfEmailPrefix = encoded.indexOf(PERSON_DATA_PREFIX_EMAIL); 1006 | 1007 | // email is last arg, target is from prefix to end of string 1008 | if (indexOfEmailPrefix > indexOfPhonePrefix) { 1009 | return removePrefix(encoded.substring(indexOfEmailPrefix, encoded.length()).trim(), 1010 | PERSON_DATA_PREFIX_EMAIL); 1011 | 1012 | // email is middle arg, target is from own prefix to next prefix 1013 | } else { 1014 | return removePrefix( 1015 | encoded.substring(indexOfEmailPrefix, indexOfPhonePrefix).trim(), 1016 | PERSON_DATA_PREFIX_EMAIL); 1017 | } 1018 | } 1019 | 1020 | /** 1021 | * Returns true if the given person's data fields are valid 1022 | * 1023 | * @param person HashMap representing the person (used in internal data) 1024 | */ 1025 | private static boolean isPersonDataValid(HashMap person) { 1026 | return isPersonNameValid(person.get(PersonProperty.NAME)) 1027 | && isPersonPhoneValid(person.get(PersonProperty.PHONE)) 1028 | && isPersonEmailValid(person.get(PersonProperty.EMAIL)); 1029 | } 1030 | 1031 | /* 1032 | * NOTE : ============================================================= 1033 | * Note the use of 'regular expressions' in the method below. 1034 | * Regular expressions can be very useful in checking if a a string 1035 | * follows a specific format. 1036 | * ==================================================================== 1037 | */ 1038 | 1039 | /** 1040 | * Returns true if the given string as a legal person name 1041 | * 1042 | * @param name to be validated 1043 | */ 1044 | private static boolean isPersonNameValid(String name) { 1045 | return name.matches("(\\w|\\s)+"); // name is nonempty mixture of alphabets and whitespace 1046 | //TODO: implement a more permissive validation 1047 | } 1048 | 1049 | /** 1050 | * Returns true if the given string as a legal person phone number 1051 | * 1052 | * @param phone to be validated 1053 | */ 1054 | private static boolean isPersonPhoneValid(String phone) { 1055 | return phone.matches("\\d+"); // phone nonempty sequence of digits 1056 | //TODO: implement a more permissive validation 1057 | } 1058 | 1059 | /** 1060 | * Returns true if the given string is a legal person email 1061 | * 1062 | * @param email to be validated 1063 | * @return whether arg is a valid person email 1064 | */ 1065 | private static boolean isPersonEmailValid(String email) { 1066 | return email.matches("\\S+@\\S+\\.\\S+"); // email is [non-whitespace]@[non-whitespace].[non-whitespace] 1067 | //TODO: implement a more permissive validation 1068 | } 1069 | 1070 | 1071 | /* 1072 | * =============================================== 1073 | * COMMAND HELP INFO FOR USERS 1074 | * =============================================== 1075 | */ 1076 | 1077 | /** 1078 | * Returns usage info for all commands 1079 | */ 1080 | private static String getUsageInfoForAllCommands() { 1081 | return getUsageInfoForAddCommand() + LS 1082 | + getUsageInfoForFindCommand() + LS 1083 | + getUsageInfoForViewCommand() + LS 1084 | + getUsageInfoForDeleteCommand() + LS 1085 | + getUsageInfoForClearCommand() + LS 1086 | + getUsageInfoForExitCommand() + LS 1087 | + getUsageInfoForHelpCommand(); 1088 | } 1089 | 1090 | /** 1091 | * Returns the string for showing 'add' command usage instruction 1092 | */ 1093 | private static String getUsageInfoForAddCommand() { 1094 | return String.format(MESSAGE_COMMAND_HELP, COMMAND_ADD_WORD, COMMAND_ADD_DESC) + LS 1095 | + String.format(MESSAGE_COMMAND_HELP_PARAMETERS, COMMAND_ADD_PARAMETERS) + LS 1096 | + String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_ADD_EXAMPLE) + LS; 1097 | } 1098 | 1099 | /** 1100 | * Returns the string for showing 'find' command usage instruction 1101 | */ 1102 | private static String getUsageInfoForFindCommand() { 1103 | return String.format(MESSAGE_COMMAND_HELP, COMMAND_FIND_WORD, COMMAND_FIND_DESC) + LS 1104 | + String.format(MESSAGE_COMMAND_HELP_PARAMETERS, COMMAND_FIND_PARAMETERS) + LS 1105 | + String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_FIND_EXAMPLE) + LS; 1106 | } 1107 | 1108 | /** 1109 | * Returns the string for showing 'delete' command usage instruction 1110 | */ 1111 | private static String getUsageInfoForDeleteCommand() { 1112 | return String.format(MESSAGE_COMMAND_HELP, COMMAND_DELETE_WORD, COMMAND_DELETE_DESC) + LS 1113 | + String.format(MESSAGE_COMMAND_HELP_PARAMETERS, COMMAND_DELETE_PARAMETER) + LS 1114 | + String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_DELETE_EXAMPLE) + LS; 1115 | } 1116 | 1117 | /** 1118 | * Returns string for showing 'clear' command usage instruction 1119 | */ 1120 | private static String getUsageInfoForClearCommand() { 1121 | return String.format(MESSAGE_COMMAND_HELP, COMMAND_CLEAR_WORD, COMMAND_CLEAR_DESC) + LS 1122 | + String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_CLEAR_EXAMPLE) + LS; 1123 | } 1124 | 1125 | /** 1126 | * Returns the string for showing 'view' command usage instruction 1127 | */ 1128 | private static String getUsageInfoForViewCommand() { 1129 | return String.format(MESSAGE_COMMAND_HELP, COMMAND_LIST_WORD, COMMAND_LIST_DESC) + LS 1130 | + String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_LIST_EXAMPLE) + LS; 1131 | } 1132 | 1133 | /** 1134 | * Returns string for showing 'help' command usage instruction 1135 | */ 1136 | private static String getUsageInfoForHelpCommand() { 1137 | return String.format(MESSAGE_COMMAND_HELP, COMMAND_HELP_WORD, COMMAND_HELP_DESC) 1138 | + String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_HELP_EXAMPLE); 1139 | } 1140 | 1141 | /** 1142 | * Returns the string for showing 'exit' command usage instruction 1143 | */ 1144 | private static String getUsageInfoForExitCommand() { 1145 | return String.format(MESSAGE_COMMAND_HELP, COMMAND_EXIT_WORD, COMMAND_EXIT_DESC) 1146 | + String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_EXIT_EXAMPLE); 1147 | } 1148 | 1149 | 1150 | /* 1151 | * ============================ 1152 | * UTILITY METHODS 1153 | * ============================ 1154 | */ 1155 | 1156 | /** 1157 | * Removes prefix from the given fullString if prefix occurs at the start of the string. 1158 | * 1159 | * @param fullString Parameter as a string 1160 | * @param prefix Parameter sign to be removed 1161 | * @return string without the sign 1162 | */ 1163 | private static String removePrefix(String fullString, String prefix) { 1164 | return fullString.replaceFirst(prefix, ""); 1165 | } 1166 | 1167 | /** 1168 | * Splits a source string into the list of substrings that were separated by whitespace. 1169 | * 1170 | * @param toSplit source string 1171 | * @return split by whitespace 1172 | */ 1173 | private static ArrayList splitByWhitespace(String toSplit) { 1174 | return new ArrayList<>(Arrays.asList(toSplit.trim().split("\\s+"))); 1175 | } 1176 | 1177 | /** 1178 | * Convert all strings in a Set to lower case 1179 | * 1180 | * @param toConvert source collection 1181 | * @return collection with strings converted to lower case 1182 | */ 1183 | private static Set convertToLowerCase(Collection toConvert) { 1184 | Set convertedStrings = new HashSet<>(); 1185 | Iterator ite = toConvert.iterator(); 1186 | while (ite.hasNext()) { 1187 | convertedStrings.add(ite.next().toLowerCase()); 1188 | } 1189 | return convertedStrings; 1190 | } 1191 | 1192 | } --------------------------------------------------------------------------------