/
AddressBook.java
1174 lines (1042 loc) · 46.5 KB
/
AddressBook.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package seedu.addressbook;
/*
* NOTE : =============================================================
* This class is written in a procedural fashion (i.e. not Object-Oriented)
* Yes, it is possible to write non-OO code using an OO language.
* ====================================================================
*/
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
/*
* NOTE : =============================================================
* This class header comment below is brief because details of how to
* use this class are documented elsewhere.
* ====================================================================
*/
/**
* This class is used to maintain a list of person data which are saved
* in a text file.
**/
public class AddressBook {
/**
* Default file path used if the user doesn't provide the file name.
*/
private static final String DEFAULT_STORAGE_FILEPATH = "addressbook.txt";
/**
* Version info of the program.
*/
private static final String VERSION = "AddessBook Level 1 - Version 1.0";
/**
* A decorative prefix added to the beginning of lines printed by AddressBook
*/
private static final String LINE_PREFIX = "|| ";
/**
* A platform independent line separator.
*/
private static final String LS = System.lineSeparator() + LINE_PREFIX;
/*
* NOTE : ==================================================================
* These messages shown to the user are defined in one place for convenient
* editing and proof reading. Such messages are considered part of the UI
* and may be subjected to review by UI experts or technical writers. Note
* that Some of the strings below include '%1$s' etc to mark the locations
* at which java String.format(...) method can insert values.
* =========================================================================
*/
private static final String MESSAGE_ADDED = "New person added: %1$s, Phone: %2$s, Email: %3$s";
private static final String MESSAGE_ADDRESSBOOK_CLEARED = "Address book has been cleared!";
private static final String MESSAGE_COMMAND_HELP = "%1$s: %2$s";
private static final String MESSAGE_COMMAND_HELP_PARAMETERS = "\tParameters: %1$s";
private static final String MESSAGE_COMMAND_HELP_EXAMPLE = "\tExample: %1$s";
private static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s";
private static final String MESSAGE_DISPLAY_PERSON_DATA = "%1$s Phone Number: %2$s Email: %3$s";
private static final String MESSAGE_DISPLAY_LIST_ELEMENT_INDEX = "%1$d. ";
private static final String MESSAGE_GOODBYE = "Exiting Address Book... Good bye!";
private static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format: %1$s " + LS + "%2$s";
private static final String MESSAGE_INVALID_FILE = "The given file name [%1$s] is not a valid file name!";
private static final String MESSAGE_INVALID_PROGRAM_ARGS = "Too many parameters! Correct program argument format:"
+ LS + "\tjava AddressBook"
+ LS + "\tjava AddressBook [custom storage file path]";
private static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid";
private static final String MESSAGE_INVALID_STORAGE_FILE_CONTENT = "Storage file has invalid content";
private static final String MESSAGE_PERSON_NOT_IN_ADDRESSBOOK = "Person could not be found in address book";
private static final String MESSAGE_ERROR_CREATING_STORAGE_FILE = "Error: unable to create file: %1$s";
private static final String MESSAGE_ERROR_MISSING_STORAGE_FILE = "Storage file missing: %1$s";
private static final String MESSAGE_ERROR_READING_FROM_FILE = "Unexpected error: unable to read from file: %1$s";
private static final String MESSAGE_ERROR_WRITING_TO_FILE = "Unexpected error: unable to write to file: %1$s";
private static final String MESSAGE_PERSONS_FOUND_OVERVIEW = "%1$d persons found!";
private static final String MESSAGE_STORAGE_FILE_CREATED = "Created new empty storage file: %1$s";
private static final String MESSAGE_WELCOME = "Welcome to your Address Book!";
private static final String MESSAGE_USING_DEFAULT_FILE = "Using default storage file : " + DEFAULT_STORAGE_FILEPATH;
// These are the prefix strings to define the data type of a command parameter
private static final String PERSON_DATA_PREFIX_PHONE = "p/";
private static final String PERSON_DATA_PREFIX_EMAIL = "e/";
private static final String PERSON_STRING_REPRESENTATION = "%1$s " // name
+ PERSON_DATA_PREFIX_PHONE + "%2$s " // phone
+ PERSON_DATA_PREFIX_EMAIL + "%3$s"; // email
private static final String COMMAND_ADD_WORD = "add";
private static final String COMMAND_ADD_DESC = "Adds a person to the address book.";
private static final String COMMAND_ADD_PARAMETERS = "NAME "
+ PERSON_DATA_PREFIX_PHONE + "PHONE_NUMBER "
+ PERSON_DATA_PREFIX_EMAIL + "EMAIL";
private static final String COMMAND_ADD_EXAMPLE = COMMAND_ADD_WORD + " John Doe p/98765432 e/johnd@gmail.com";
private static final String COMMAND_FIND_WORD = "find";
private static final String COMMAND_FIND_DESC = "Finds all persons whose names contain any of the specified "
+ "keywords (case-sensitive) and displays them as a list with index numbers.";
private static final String COMMAND_FIND_PARAMETERS = "KEYWORD [MORE_KEYWORDS]";
private static final String COMMAND_FIND_EXAMPLE = COMMAND_FIND_WORD + " alice bob charlie";
private static final String COMMAND_LIST_WORD = "list";
private static final String COMMAND_LIST_DESC = "Displays all persons as a list with index numbers.";
private static final String COMMAND_LIST_EXAMPLE = COMMAND_LIST_WORD;
private static final String COMMAND_DELETE_WORD = "delete";
private static final String COMMAND_DELETE_DESC = "Deletes a person identified by the index number used in "
+ "the last find/list call.";
private static final String COMMAND_DELETE_PARAMETER = "INDEX";
private static final String COMMAND_DELETE_EXAMPLE = COMMAND_DELETE_WORD + " 1";
private static final String COMMAND_CLEAR_WORD = "clear";
private static final String COMMAND_CLEAR_DESC = "Clears address book permanently.";
private static final String COMMAND_CLEAR_EXAMPLE = COMMAND_CLEAR_WORD;
private static final String COMMAND_HELP_WORD = "help";
private static final String COMMAND_HELP_DESC = "Shows program usage instructions.";
private static final String COMMAND_HELP_EXAMPLE = COMMAND_HELP_WORD;
private static final String COMMAND_EXIT_WORD = "exit";
private static final String COMMAND_EXIT_DESC = "Exits the program.";
private static final String COMMAND_EXIT_EXAMPLE = COMMAND_EXIT_WORD;
private static final String DIVIDER = "===================================================";
/* We use a String array to store details of a single person.
* The constants given below are the indexes for the different data elements of a person
* used by the internal String[] storage format.
* For example, a person's name is stored as the 0th element in the array.
*/
private static final int PERSON_DATA_INDEX_NAME = 0;
private static final int PERSON_DATA_INDEX_PHONE = 1;
private static final int PERSON_DATA_INDEX_EMAIL = 2;
/**
* The number of data elements for a single person.
*/
private static final int PERSON_DATA_COUNT = 3;
/**
* Offset required to convert between 1-indexing and 0-indexing.COMMAND_
*/
private static final int DISPLAYED_INDEX_OFFSET = 1;
/**
* If the first non-whitespace character in a user's input line is this, that line will be ignored.
*/
private static final char INPUT_COMMENT_MARKER = '#';
/*
* This variable is declared for the whole class (instead of declaring it
* inside the readUserCommand() method to facilitate automated testing using
* the I/O redirection technique. If not, only the first line of the input
* text file will be processed.
*/
private static final Scanner SCANNER = new Scanner(System.in);
/*
* NOTE : =============================================================================================
* Note that the type of the variable below can also be declared as List<String[]>, as follows:
* private static final List<String[]> ALL_PERSONS = new ArrayList<>()
* That is because List is an interface implemented by the ArrayList class.
* In this code we use ArrayList instead because we wanted to to stay away from advanced concepts
* such as interface inheritance.
* ====================================================================================================
*/
/**
* List of all persons in the address book.
*/
private static final ArrayList<String[]> ALL_PERSONS = new ArrayList<>();
/**
* Stores the most recent list of persons shown to the user as a result of a user command.
* This is a subset of the full list. Deleting persons in the pull list does not delete
* those persons from this list.
*/
private static ArrayList<String[]> latestPersonListingView = getAllPersonsInAddressBook(); // initial view is of all
/**
* The path to the file used for storing person data.
*/
private static String storageFilePath;
/*
* NOTE : =============================================================
* Notice how this method solves the whole problem at a very high level.
* We can understand the high-level logic of the program by reading this
* method alone.
* If the reader wants a deeper understanding of the solution, she can go
* to the next level of abstraction by reading the methods that are
* referenced by the high-level method below.
* ====================================================================
*/
/**
* Main entry point of the application.
* Initializes the application and starts the interaction with the user.
*/
public static void main(String[] args) {
showWelcomeMessage();
processProgramArgs(args);
loadDataFromStorage();
while (true) {
String userCommand = getUserInput();
echoUserCommand(userCommand);
String feedback = executeCommand(userCommand);
showResultToUser(feedback);
}
}
/*
* NOTE : =============================================================
* The method header comment can be omitted if the method is trivial
* and the header comment is going to be almost identical to the method
* signature anyway.
* ====================================================================
*/
private static void showWelcomeMessage() {
showToUser(DIVIDER, DIVIDER, VERSION, MESSAGE_WELCOME, DIVIDER);
}
private static void showResultToUser(String result) {
showToUser(result, DIVIDER);
}
/*
* NOTE : =============================================================
* Parameter description can be omitted from the method header comment
* if the parameter name is self-explanatory.
* In the method below, '@param userInput' comment has been omitted.
* ====================================================================
*/
/**
* Echoes the user input back to the user.
*/
private static void echoUserCommand(String userCommand) {
showToUser("[Command entered:" + userCommand + "]");
}
/**
* Processes the program main method run arguments.
* If a valid storage file is specified, sets up that file for storage.
* Otherwise sets up the default file for storage.
*
* @param args full program arguments passed to application main method
*/
private static void processProgramArgs(String[] args) {
if (args.length >= 2) {
showToUser(MESSAGE_INVALID_PROGRAM_ARGS);
exitProgram();
}
if (args.length == 1) {
setupGivenFileForStorage(args[0]);
}
if (args.length == 0) {
setupDefaultFileForStorage();
}
}
/**
* Sets up the storage file based on the supplied file path.
* Creates the file if it is missing.
* Exits if the file name is not acceptable.
*/
private static void setupGivenFileForStorage(String filePath) {
if (!isValidFilePath(filePath)) {
showToUser(String.format(MESSAGE_INVALID_FILE, filePath));
exitProgram();
}
storageFilePath = filePath;
createFileIfMissing(filePath);
}
/**
* Displays the goodbye message and exits the runtime.
*/
private static void exitProgram() {
showToUser(MESSAGE_GOODBYE, DIVIDER, DIVIDER);
System.exit(0);
}
/**
* Sets up the storage based on the default file.
* Creates file if missing.
* Exits program if the file cannot be created.
*/
private static void setupDefaultFileForStorage() {
showToUser(MESSAGE_USING_DEFAULT_FILE);
storageFilePath = DEFAULT_STORAGE_FILEPATH;
createFileIfMissing(storageFilePath);
}
/**
* Returns true if the given file path is valid.
* A file path is valid if it has a valid parent directory as determined by {@link #hasValidParentDirectory}
* and a valid file name as determined by {@link #hasValidFileName}.
*/
private static boolean isValidFilePath(String filePath) {
if (filePath == null) {
return false;
}
Path filePathToValidate;
try {
filePathToValidate = Paths.get(filePath);
} catch (InvalidPathException ipe) {
return false;
}
return hasValidParentDirectory(filePathToValidate) && hasValidFileName(filePathToValidate);
}
/**
* Returns true if the file path has a parent directory that exists.
*/
private static boolean hasValidParentDirectory(Path filePath) {
Path parentDirectory = filePath.getParent();
return parentDirectory == null || Files.isDirectory(parentDirectory);
}
/**
* Returns true if file path has a valid file name.
* File name is valid if it has an extension and no reserved characters.
* Reserved characters are OS-dependent.
* If a file already exists, it must be a regular file.
*/
private static boolean hasValidFileName(Path filePath) {
return filePath.getFileName().toString().lastIndexOf('.') > 0
&& (!Files.exists(filePath) || Files.isRegularFile(filePath));
}
/**
* Initialises the in-memory data using the storage file.
* Assumption: The file exists.
*/
private static void loadDataFromStorage() {
initialiseAddressBookModel(loadPersonsFromFile(storageFilePath));
}
/*
* ===========================================
* COMMAND LOGIC
* ===========================================
*/
/**
* Executes the command as specified by the {@code userInputString}
*
* @param userInputString raw input from user
* @return feedback about how the command was executed
*/
private static String executeCommand(String userInputString) {
final String[] commandTypeAndParams = splitCommandWordAndArgs(userInputString);
final String commandType = commandTypeAndParams[0];
final String commandArgs = commandTypeAndParams[1];
switch (commandType) {
case COMMAND_ADD_WORD:
return executeAddPerson(commandArgs);
case COMMAND_FIND_WORD:
return executeFindPersons(commandArgs);
case COMMAND_LIST_WORD:
return executeListAllPersonsInAddressBook();
case COMMAND_DELETE_WORD:
return executeDeletePerson(commandArgs);
case COMMAND_CLEAR_WORD:
return executeClearAddressBook();
case COMMAND_HELP_WORD:
return getUsageInfoForAllCommands();
case COMMAND_EXIT_WORD:
executeExitProgramRequest();
// Fallthrough
default:
return getMessageForInvalidCommandInput(commandType, getUsageInfoForAllCommands());
}
}
/**
* Splits raw user input into command word and command arguments string
*
* @return size 2 array; first element is the command type and second element is the arguments string
*/
private static String[] splitCommandWordAndArgs(String rawUserInput) {
final String[] split = rawUserInput.trim().split("\\s+", 2);
return split.length == 2 ? split : new String[] { split[0] , "" }; // else case: no parameters
}
/**
* Constructs a generic feedback message for an invalid command from user, with instructions for correct usage.
*
* @param correctUsageInfo message showing the correct usage
* @return invalid command args feedback message
*/
private static String getMessageForInvalidCommandInput(String userCommand, String correctUsageInfo) {
return String.format(MESSAGE_INVALID_COMMAND_FORMAT, userCommand, correctUsageInfo);
}
/**
* Adds a person (specified by the command args) to the address book.
* The entire command arguments string is treated as a string representation of the person to add.
*
* @param commandArgs full command args string from the user
* @return feedback display message for the operation result
*/
private static String executeAddPerson(String commandArgs) {
// try decoding a person from the raw args
final Optional<String[]> decodeResult = decodePersonFromString(commandArgs);
// checks if args are valid (decode result will not be present if the person is invalid)
if (!decodeResult.isPresent()) {
return getMessageForInvalidCommandInput(COMMAND_ADD_WORD, getUsageInfoForAddCommand());
}
// add the person as specified
final String[] personToAdd = decodeResult.get();
addPersonToAddressBook(personToAdd);
return getMessageForSuccessfulAddPerson(personToAdd);
}
/**
* Constructs a feedback message for a successful add person command execution.
*
* @param addedPerson person who was successfully added
* @return successful add person feedback message
* @see #executeAddPerson(String)
*/
private static String getMessageForSuccessfulAddPerson(String[] addedPerson) {
return String.format(MESSAGE_ADDED,
getNameFromPerson(addedPerson), getPhoneFromPerson(addedPerson), getEmailFromPerson(addedPerson));
}
/**
* Finds and lists all persons in address book whose name contains any of the argument keywords.
* Keyword matching is case sensitive.
*
* @param commandArgs full command args string from the user
* @return feedback display message for the operation result
*/
private static String executeFindPersons(String commandArgs) {
final Set<String> keywords = extractKeywordsFromFindPersonArgs(commandArgs);
final ArrayList<String[]> personsFound = getPersonsWithNameContainingAnyKeyword(keywords);
showToUser(personsFound);
return getMessageForPersonsDisplayedSummary(personsFound);
}
/**
* Constructs a feedback message to summarise an operation that displayed a listing of persons.
*
* @param personsDisplayed used to generate summary
* @return summary message for persons displayed
*/
private static String getMessageForPersonsDisplayedSummary(ArrayList<String[]> personsDisplayed) {
return String.format(MESSAGE_PERSONS_FOUND_OVERVIEW, personsDisplayed.size());
}
/**
* Extracts keywords from the command arguments given for the find persons command.
*
* @param findPersonCommandArgs full command args string for the find persons command
* @return set of keywords as specified by args
*/
private static Set<String> extractKeywordsFromFindPersonArgs(String findPersonCommandArgs) {
return new HashSet<>(splitByWhitespace(findPersonCommandArgs.trim()));
}
/**
* Retrieves all persons in the full model whose names contain some of the specified keywords.
*
* @param keywords for searching
* @return list of persons in full model with name containing some of the keywords
*/
private static ArrayList<String[]> getPersonsWithNameContainingAnyKeyword(Collection<String> keywords) {
final ArrayList<String[]> matchedPersons = new ArrayList<>();
for (String[] person : getAllPersonsInAddressBook()) {
final Set<String> wordsInName = new HashSet<>(splitByWhitespace(getNameFromPerson(person)));
if (!Collections.disjoint(wordsInName, keywords)) {
matchedPersons.add(person);
}
}
return matchedPersons;
}
/**
* Deletes person identified using last displayed index.
*
* @param commandArgs full command args string from the user
* @return feedback display message for the operation result
*/
private static String executeDeletePerson(String commandArgs) {
if (!isDeletePersonArgsValid(commandArgs)) {
return getMessageForInvalidCommandInput(COMMAND_DELETE_WORD, getUsageInfoForDeleteCommand());
}
final int targetVisibleIndex = extractTargetIndexFromDeletePersonArgs(commandArgs);
if (!isDisplayIndexValidForLastPersonListingView(targetVisibleIndex)) {
return MESSAGE_INVALID_PERSON_DISPLAYED_INDEX;
}
final String[] targetInModel = getPersonByLastVisibleIndex(targetVisibleIndex);
return deletePersonFromAddressBook(targetInModel) ? getMessageForSuccessfulDelete(targetInModel) // success
: MESSAGE_PERSON_NOT_IN_ADDRESSBOOK; // not found
}
/**
* Checks validity of delete person argument string's format.
*
* @param rawArgs raw command args string for the delete person command
* @return whether the input args string is valid
*/
private static boolean isDeletePersonArgsValid(String rawArgs) {
try {
final int extractedIndex = Integer.parseInt(rawArgs.trim()); // use standard libraries to parse
return extractedIndex >= DISPLAYED_INDEX_OFFSET;
} catch (NumberFormatException nfe) {
return false;
}
}
/**
* Extracts the target's index from the raw delete person args string
*
* @param rawArgs raw command args string for the delete person command
* @return extracted index
*/
private static int extractTargetIndexFromDeletePersonArgs(String rawArgs) {
return Integer.parseInt(rawArgs.trim());
}
/**
* Checks that the given index is within bounds and valid for the last shown person list view.
*
* @param index to check
* @return whether it is valid
*/
private static boolean isDisplayIndexValidForLastPersonListingView(int index) {
return index >= DISPLAYED_INDEX_OFFSET && index < latestPersonListingView.size() + DISPLAYED_INDEX_OFFSET;
}
/**
* Constructs a feedback message for a successful delete person command execution.
*
* @param deletedPerson successfully deleted
* @return successful delete person feedback message
* @see #executeDeletePerson(String)
*/
private static String getMessageForSuccessfulDelete(String[] deletedPerson) {
return String.format(MESSAGE_DELETE_PERSON_SUCCESS, getMessageForFormattedPersonData(deletedPerson));
}
/**
* Clears all persons in the address book.
*
* @return feedback display message for the operation result
*/
private static String executeClearAddressBook() {
clearAddressBook();
return MESSAGE_ADDRESSBOOK_CLEARED;
}
/**
* Displays all persons in the address book to the user; in added order.
*
* @return feedback display message for the operation result
*/
private static String executeListAllPersonsInAddressBook() {
ArrayList<String[]> toBeDisplayed = getAllPersonsInAddressBook();
showToUser(toBeDisplayed);
return getMessageForPersonsDisplayedSummary(toBeDisplayed);
}
/**
* Requests to terminate the program.
*/
private static void executeExitProgramRequest() {
exitProgram();
}
/*
* ===========================================
* UI LOGIC
* ===========================================
*/
/**
* Prompts for the command and reads the text entered by the user.
* Ignores lines with first non-whitespace char equal to {@link #INPUT_COMMENT_MARKER} (considered comments)
*
* @return full line entered by the user
*/
private static String getUserInput() {
System.out.print(LINE_PREFIX + "Enter command: ");
String inputLine = SCANNER.nextLine();
// silently consume all blank and comment lines
while (inputLine.trim().isEmpty() || inputLine.trim().charAt(0) == INPUT_COMMENT_MARKER) {
inputLine = SCANNER.nextLine();
}
return inputLine;
}
/*
* NOTE : =============================================================
* Note how the method below uses Java 'Varargs' feature so that the
* method can accept a varying number of message parameters.
* ====================================================================
*/
/**
* Shows a message to the user
*/
private static void showToUser(String... message) {
for (String m : message) {
System.out.println(LINE_PREFIX + m);
}
}
/**
* Shows the list of persons to the user.
* The list will be indexed, starting from 1.
*
*/
private static void showToUser(ArrayList<String[]> persons) {
String listAsString = getDisplayString(persons);
showToUser(listAsString);
updateLatestViewedPersonListing(persons);
}
/**
* Returns the display string representation of the list of persons.
*/
private static String getDisplayString(ArrayList<String[]> persons) {
final StringBuilder messageAccumulator = new StringBuilder();
for (int i = 0; i < persons.size(); i++) {
final String[] person = persons.get(i);
final int displayIndex = i + DISPLAYED_INDEX_OFFSET;
messageAccumulator.append('\t')
.append(getIndexedPersonListElementMessage(displayIndex, person))
.append(LS);
}
return messageAccumulator.toString();
}
/**
* Constructs a prettified listing element message to represent a person and their data.
*
* @param visibleIndex visible index for this listing
* @param person to show
* @return formatted listing message with index
*/
private static String getIndexedPersonListElementMessage(int visibleIndex, String[] person) {
return String.format(MESSAGE_DISPLAY_LIST_ELEMENT_INDEX, visibleIndex)
+ getMessageForFormattedPersonData(person);
}
/**
* Constructs a prettified string to show the user a person's data.
*
* @param person to show
* @return formatted message showing internal state
*/
private static String getMessageForFormattedPersonData(String[] person) {
return String.format(MESSAGE_DISPLAY_PERSON_DATA,
getNameFromPerson(person), getPhoneFromPerson(person), getEmailFromPerson(person));
}
/**
* Updates the latest person listing view the user has seen.
*
* @param newListing the new listing of persons
*/
private static void updateLatestViewedPersonListing(ArrayList<String[]> newListing) {
// clone to insulate from future changes to arg list
latestPersonListingView = new ArrayList<>(newListing);
}
/**
* Retrieves the person identified by the displayed index from the last shown listing of persons.
*
* @param lastVisibleIndex displayed index from last shown person listing
* @return the actual person object in the last shown person listing
*/
private static String[] getPersonByLastVisibleIndex(int lastVisibleIndex) {
return latestPersonListingView.get(lastVisibleIndex - DISPLAYED_INDEX_OFFSET);
}
/*
* ===========================================
* STORAGE LOGIC
* ===========================================
*/
/**
* Creates storage file if it does not exist. Shows feedback to user.
*
* @param filePath file to create if not present
*/
private static void createFileIfMissing(String filePath) {
final File storageFile = new File(filePath);
if (storageFile.exists()) {
return;
}
showToUser(String.format(MESSAGE_ERROR_MISSING_STORAGE_FILE, filePath));
try {
storageFile.createNewFile();
showToUser(String.format(MESSAGE_STORAGE_FILE_CREATED, filePath));
} catch (IOException ioe) {
showToUser(String.format(MESSAGE_ERROR_CREATING_STORAGE_FILE, filePath));
exitProgram();
}
}
/**
* Converts contents of a file into a list of persons.
* Shows error messages and exits program if any errors in reading or decoding was encountered.
*
* @param filePath file to load from
* @return the list of decoded persons
*/
private static ArrayList<String[]> loadPersonsFromFile(String filePath) {
final Optional<ArrayList<String[]>> successfullyDecoded = decodePersonsFromStrings(getLinesInFile(filePath));
if (!successfullyDecoded.isPresent()) {
showToUser(MESSAGE_INVALID_STORAGE_FILE_CONTENT);
exitProgram();
}
return successfullyDecoded.get();
}
/**
* Gets all lines in the specified file as a list of strings. Line separators are removed.
* Shows error messages and exits program if unable to read from file.
*/
private static ArrayList<String> getLinesInFile(String filePath) {
ArrayList<String> lines = null;
try {
lines = new ArrayList<>(Files.readAllLines(Paths.get(filePath)));
} catch (FileNotFoundException fnfe) {
showToUser(String.format(MESSAGE_ERROR_MISSING_STORAGE_FILE, filePath));
exitProgram();
} catch (IOException ioe) {
showToUser(String.format(MESSAGE_ERROR_READING_FROM_FILE, filePath));
exitProgram();
}
return lines;
}
/**
* Saves all data to the file. Exits program if there is an error saving to file.
*
* @param filePath file for saving
*/
private static void savePersonsToFile(ArrayList<String[]> persons, String filePath) {
final ArrayList<String> linesToWrite = encodePersonsToStrings(persons);
try {
Files.write(Paths.get(storageFilePath), linesToWrite);
} catch (IOException ioe) {
showToUser(String.format(MESSAGE_ERROR_WRITING_TO_FILE, filePath));
exitProgram();
}
}
/*
* ================================================================================
* INTERNAL ADDRESS BOOK DATA METHODS
* ================================================================================
*/
/**
* Adds a person to the address book. Saves changes to storage file.
*
* @param person to add
*/
private static void addPersonToAddressBook(String[] person) {
ALL_PERSONS.add(person);
savePersonsToFile(getAllPersonsInAddressBook(), storageFilePath);
}
/**
* Deletes the specified person from the addressbook if it is inside. Saves any changes to storage file.
*
* @param exactPerson the actual person inside the address book
* (exactPerson == the person to delete in the full list)
* @return true if the given person was found and deleted in the model
*/
private static boolean deletePersonFromAddressBook(String[] exactPerson) {
final boolean changed = ALL_PERSONS.remove(exactPerson);
if (changed) {
savePersonsToFile(getAllPersonsInAddressBook(), storageFilePath);
}
return changed;
}
/**
* Returns all persons in the address book
*/
private static ArrayList<String[]> getAllPersonsInAddressBook() {
return ALL_PERSONS;
}
/**
* Clears all persons in the address book and saves changes to file.
*/
private static void clearAddressBook() {
ALL_PERSONS.clear();
savePersonsToFile(getAllPersonsInAddressBook(), storageFilePath);
}
/**
* Resets the internal model with the given data. Does not save to file.
*
* @param persons list of persons to initialise the model with
*/
private static void initialiseAddressBookModel(ArrayList<String[]> persons) {
ALL_PERSONS.clear();
ALL_PERSONS.addAll(persons);
}
/*
* ===========================================
* PERSON METHODS
* ===========================================
*/
/**
* Returns the given person's name
*
* @param person whose name you want
*/
private static String getNameFromPerson(String[] person) {
return person[PERSON_DATA_INDEX_NAME];
}
/**
* Returns given person's phone number
*
* @param person whose phone number you want
*/
private static String getPhoneFromPerson(String[] person) {
return person[PERSON_DATA_INDEX_PHONE];
}
/**
* Returns given person's email
*
* @param person whose email you want
*/
private static String getEmailFromPerson(String[] person) {
return person[PERSON_DATA_INDEX_EMAIL];
}
/**
* Creates a person from the given data.
*
* @param name of person
* @param phone without data prefix
* @param email without data prefix
* @return constructed person
*/
private static String[] makePersonFromData(String name, String phone, String email) {
final String[] person = new String[PERSON_DATA_COUNT];
person[PERSON_DATA_INDEX_NAME] = name;
person[PERSON_DATA_INDEX_PHONE] = phone;
person[PERSON_DATA_INDEX_EMAIL] = email;
return person;
}
/**
* Encodes a person into a decodable and readable string representation.
*
* @param person to be encoded
* @return encoded string
*/
private static String encodePersonToString(String[] person) {
return String.format(PERSON_STRING_REPRESENTATION,
getNameFromPerson(person), getPhoneFromPerson(person), getEmailFromPerson(person));
}
/**
* Encodes list of persons into list of decodable and readable string representations.
*
* @param persons to be encoded
* @return encoded strings
*/
private static ArrayList<String> encodePersonsToStrings(ArrayList<String[]> persons) {
final ArrayList<String> encoded = new ArrayList<>();
for (String[] person : persons) {
encoded.add(encodePersonToString(person));
}
return encoded;
}
/*
* NOTE : =============================================================
* Note the use of Java's new 'Optional' feature to indicate that
* the return value may not always be present.
* ====================================================================
*/
/**
* Decodes a person from it's supposed string representation.
*
* @param encoded string to be decoded
* @return if cannot decode: empty Optional
* else: Optional containing decoded person
*/
private static Optional<String[]> decodePersonFromString(String encoded) {
// check that we can extract the parts of a person from the encoded string
if (!isPersonDataExtractableFrom(encoded)) {
return Optional.empty();
}
final String[] decodedPerson = makePersonFromData(
extractNameFromPersonString(encoded),
extractPhoneFromPersonString(encoded),
extractEmailFromPersonString(encoded)
);
// check that the constructed person is valid
return isPersonDataValid(decodedPerson) ? Optional.of(decodedPerson) : Optional.empty();
}
/**
* Decodes persons from a list of string representations.
*
* @param encodedPersons strings to be decoded
* @return if cannot decode any: empty Optional
* else: Optional containing decoded persons
*/
private static Optional<ArrayList<String[]>> decodePersonsFromStrings(ArrayList<String> encodedPersons) {
final ArrayList<String[]> decodedPersons = new ArrayList<>();
for (String encodedPerson : encodedPersons) {
final Optional<String[]> decodedPerson = decodePersonFromString(encodedPerson);
if (!decodedPerson.isPresent()) {
return Optional.empty();
}
decodedPersons.add(decodedPerson.get());
}
return Optional.of(decodedPersons);
}
/**
* Returns true if person data (email, name, phone etc) can be extracted from the argument string.
* Format is [name] p/[phone] e/[email], phone and email positions can be swapped.
*
* @param personData person string representation
*/
private static boolean isPersonDataExtractableFrom(String personData) {
final String matchAnyPersonDataPrefix = PERSON_DATA_PREFIX_PHONE + '|' + PERSON_DATA_PREFIX_EMAIL;
final String[] splitArgs = personData.trim().split(matchAnyPersonDataPrefix);
return splitArgs.length == 3 // 3 arguments
&& !splitArgs[0].isEmpty() // non-empty arguments
&& !splitArgs[1].isEmpty()
&& !splitArgs[2].isEmpty();
}
/**
* Extracts substring representing person name from person string representation
*
* @param encoded person string representation
* @return name argument
*/
private static String extractNameFromPersonString(String encoded) {
final int indexOfPhonePrefix = encoded.indexOf(PERSON_DATA_PREFIX_PHONE);
final int indexOfEmailPrefix = encoded.indexOf(PERSON_DATA_PREFIX_EMAIL);
// name is leading substring up to first data prefix symbol
int indexOfFirstPrefix = Math.min(indexOfEmailPrefix, indexOfPhonePrefix);
return encoded.substring(0, indexOfFirstPrefix).trim();
}
/**
* Extracts substring representing phone number from person string representation
*
* @param encoded person string representation
* @return phone number argument WITHOUT prefix
*/
private static String extractPhoneFromPersonString(String encoded) {
final int indexOfPhonePrefix = encoded.indexOf(PERSON_DATA_PREFIX_PHONE);
final int indexOfEmailPrefix = encoded.indexOf(PERSON_DATA_PREFIX_EMAIL);
// phone is last arg, target is from prefix to end of string
if (indexOfPhonePrefix > indexOfEmailPrefix) {
return removePrefixSign(encoded.substring(indexOfPhonePrefix, encoded.length()).trim(),
PERSON_DATA_PREFIX_PHONE);