├── _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 | }
--------------------------------------------------------------------------------