Repository: kokonguyen191/addressbook-level1
Branch: master
Commit: 3405d7bea89c
Files: 10
Total size: 86.3 KB
Directory structure:
gitextract_xm6qqcx3/
├── .gitignore
├── LICENSE
├── README.md
├── _config.yml
├── src/
│ └── seedu/
│ └── addressbook/
│ └── AddressBook.java
└── test/
├── exitinput.txt
├── expected.txt
├── input.txt
├── runtests.bat
└── runtests.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Default storage file for addressbook : don't need to cleanup when running from IDE
addressbook.txt
# Compiled classfiles
*.class
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# IDEA files
.idea/
*.iml
# Temp files used for testing
test/actual.txt
test/localrun.bat
test/data/
/bin/
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 nus-oss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# AddressBook (Level 1)
* This is a CLI (Command Line Interface) Address Book application **written in procedural fashion**.
* It is a Java sample application intended for students learning Software Engineering while using Java as
the main programming language.
* It provides a **reasonably well-written** code example that is **significantly bigger** than what students
usually write in data structure modules.
* It can be used to achieve a number of beginner-level [learning outcomes](#learning-outcomes) without
running into the complications of OOP or GUI programmings.
**Table of Contents**
* [**User Guide**](#user-guide)
* [Starting the program](#starting-the-program)
* [List of commands](#list-of-commands)
* [**Developer Guide**](#developer-guide)
* [Setting Up](#setting-up)
* [Design](#design)
* [Testing](#testing)
* [**Learning Outcomes**](#learning-outcomes)
1. [Set up a project in an IDE [`LO-IdeSetup`]](#set-up-a-project-in-an-ide-lo-idesetup)
2. [Navigate code efficiently [`LO-CodeNavigation`]](#navigate-code-efficiently-lo-codenavigation)
3. [Use a debugger [`LO-Debugging`]](#use-a-debugger-lo-debugging)
4. [Automate CLI testing [`LO-AutomatedCliTesting`]](#automate-cli-testing-lo-automatedclitesting)
5. [Use Collections [`LO-Collections`]](#use-collections-lo-collections)
6. [Use Enums [`LO-Enums`]](#use-enums-lo-enums)
7. [Use Varargs [`LO-Varargs`]](#use-varargs-lo-varargs)
8. [Follow a coding standard [`LO-CodingStandard`]](#follow-a-coding-standard-lo-codingstandard)
9. [Apply coding best practices [`LO-CodingBestPractices`]](#apply-coding-best-practices-lo-codingbestpractices)
10. [Refactor code [`LO-Refactor`]](#refactor-code-lo-refactor)
11. [Abstract methods well [`LO-MethodAbstraction`]](#abstract-methods-well-lo-methodabstraction)
12. [Follow SLAP [`LO-SLAP`]](#follow-slap-lo-slap)
13. [Work in a 1kLoC code base [`LO-1KLoC`]](#work-in-a-1kloc-code-baselo-1kloc)
* [**Contributors**](#contributors)
* [**Contact Us**](#contact-us)
-----------------------------------------------------------------------------------------------------
# User Guide
This product is not meant for end-users and therefore there is no user-friendly installer.
Please refer to the [Setting up](#setting-up) section to learn how to set up the project.
## Starting the program
**Using IntelliJ**
1. Find the project in the `Project Explorer` (usually located at the left side)
1. If the `Project Explorer` is not visible, press <kbd>ALT</kbd>+<kbd>1</kbd> for Windows/Linux, <kbd>CMD</kbd>+<kbd>1</kbd> for macOS to open the `Project Explorer` tab
2. Go to the `src` folder and locate the `AddressBook` file
3. Right click the file and select `Run AddressBook.main()`
4. The program now should run on the `Console` (usually located at the bottom side)
5. Now you can interact with the program through the `Console`
**Using Command Line**
1. 'Build' the project using IntelliJ (`Build` -> `Build Project`)
2. Open the `Terminal`/`Command Prompt`. Note: You can open a terminal inside Intellij too (`View` -> `Tool Windows` -> `Terminal`)
3. `cd` into the project's `out\production\addressbook-level1` directory. Note: That is the where Intellij puts its compiled class files.
4. Type `java seedu.addressbook.AddressBook`, then <kbd>Enter</kbd> to execute
5. Now you can interact with the program through the CLI
## List of commands
#### Viewing help: `help`
Format: `help`
> Help is also shown if you enter an incorrect command e.g. `abcd`
#### Adding a person: `add`
> Adds a person to the address book
Format: `add NAME p/PHONE_NUMBER e/EMAIL`
> Words in `UPPER_CASE` are the parameters<br>
Phone number and email can be in any order but the name must come first.
Examples:
* `add John Doe p/98765432 e/johnd@gmail.com`
* `add Betsy Crowe e/bencrowe@gmail.com p/1234567 `
#### Listing all persons: `list`
> Shows a list of persons, as an indexed list, in the order they were added to the address book,
oldest first.
Format: `list`
#### Finding a person by keyword `find`
> Finds persons that match given keywords
Format: `find KEYWORD [MORE_KEYWORDS]`
> The search is case insensitive, the order of the keywords does not matter, only the name is searched,
and persons matching at least one keyword will be returned (i.e. `OR` search).
Examples:
* `find John`
> Returns `John Doe` but not `john`
* `find Betsy Tim John`
> Returns Any person having names `Betsy`, `Tim`, or `John`
#### Deleting a person: `delete`
Format: `delete INDEX`
> Deletes the person at the specified `INDEX`.
The index refers to the index numbers shown in the most recent listing.
Examples:
* `list`<br>
`delete 2`
> Deletes the 2nd person in the address book.
* `find Betsy` <br>
`delete 1`
> Deletes the 1st person in the results of the `find` command.
#### Clearing all entries: `clear`
> Clears all entries from the address book.
Format: `clear`
#### Exiting the program: `exit`
Format: `exit`
#### Saving the data
Address book data are saved in the hard disk automatically after any command that changes the data.
There is no need to save manually.
#### Changing the save location
Address book data are saved in a file called `addressbook.txt` in the project root folder.
You can change the location by specifying the file path as a program argument.
Example:
* `java seedu.addressbook.AddressBook mydata.txt`
* `java seedu.addressbook.AddressBook myFolder/mydata.txt`
> The file path must contain a valid file name and a valid parent directory.<br>
File name is valid if it has an extension and no reserved characters (OS-dependent).<br>
Parent directory is valid if it exists.<br>
Note for non-Windows users: if the file already exists, it must be a 'regular' file.<br>
> When running the program inside IntelliJ, there is a way to set command line parameters
before running the program.
-----------------------------------------------------------------------------------------------------
# Developer Guide
## Setting up
**Prerequisites**
* JDK 8 or later
* IntelliJ IDE
**Importing the project into IntelliJ**
1. Open IntelliJ (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project dialog first)
2. Set up the correct JDK version
1. Click `Configure` > `Project Defaults` > `Project Structure`
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.
3. Click `OK`.
3. Click `Import Project`
4. Locate the project directory and click `OK`
5. Select `Create project from existing sources` and click `Next`
6. Rename the project if you want. Click `Next`
7. Ensure that your src folder is checked. Keep clicking `Next`
8. Click `Finish`
## Design
AddressBook saves data in a plain text file, one line for each person, in the format `NAME p/PHONE e/EMAIL`.
Here is an example:
```
John Doe p/98765432 e/johnd@gmail.com
Jane Doe p/12346758 e/jane@gmail.com
```
All person data are loaded to memory at start up and written to the file after any command that mutates data.
In-memory data are held in a `ArrayList<String[]>` where each `String[]` object represents a person.
## Testing
**Windows**
1. Open a DOS window in the `test` folder
2. Run the `runtests.bat` script
3. If the script reports that there is no difference between `actual.txt` and `expected.txt`,
the test has passed.
**Mac/Unix/Linux**
1. Open a terminal window in the `test` folder
2. Run the `runtests.sh` script
3. If the script reports that there is no difference between `actual.txt` and `expected.txt`,
the test has passed.
**Troubleshooting test failures**
* Problem: How do I examine the exact differences between `actual.txt` and `expected.txt`?<br>
Solution: You can use a diff/merge tool with a GUI e.g. WinMerge (on Windows)
* Problem: The two files look exactly the same, but the test script reports they are different.<br>
Solution: This can happen because the line endings used by Windows is different from Unix-based
OSes. Convert the `actual.txt` to the format used by your OS using some [utility](https://kb.iu.edu/d/acux).
* Problem: Test fails during the very first time.<br>
Solution: The output of the very first test run could be slightly different because the program
creates a new storage file. Tests should pass from the 2nd run onwards.
-----------------------------------------------------------------------------------------------------
# Learning Outcomes
_Learning Outcomes_ are the things you should be able to do after studying this code and completing the
corresponding exercises.
## Set up a project in an IDE `[LO-IdeSetup]`
* Learn [how to set up a project in IntelliJ](https://se-edu.github.io/se-book/intellij/projectSetup/).
#### Exercise: Setup project in IntelliJ
Part A:
* Create a new project in IntelliJ and write a small HelloWorld program.
Part B:
* Download the source code for this project: Click on the `clone or download` link above and either,
1. download as a zip file and unzip content.
2. clone the repo (if you know how to use Git) to your Computer.
* [Set up](#setting-up) the project in IntelliJ.
* [Run the program](#starting-the-program) from within IntelliJ, and try the features described in
the [User guide](#user-guide) section.
## Navigate code efficiently `[LO-CodeNavigation]`
The `AddressBook.java` code is rather long, which makes it cumbersome to navigate by scrolling alone.
Navigating code using IDE shortcuts is a more efficient option.
For example, <kbd>CTRL</kbd>+<kbd>B</kbd> will navigate to the definition of the method/field at the cursor.
Learn [basic IntelliJ code navigation features](https://se-edu.github.io/se-book/intellij/codeNavigation/).
#### Exercise: Learn to navigate code using shortcuts
* Use Intellij basic code navigation features to navigate code of this project.
## Use a debugger `[LO-Debugging]`
Learn [basic IntelliJ debugging features](https://se-edu.github.io/se-book/intellij/debuggingBasic/).
#### Exercise: Learn to step through code using the debugger
Prerequisite: `[LO-IdeSetup]`
Demonstrate your debugging skills using the AddressBook code.
Here are some things you can do in your demonstration:
1. Set a 'break point'
2. Run the program in debug mode
3. 'Step through' a few lines of code while examining variable values
4. 'Step into', and 'step out of', methods as you step through the code
5. ...
## Automate CLI testing `[LO-AutomatedCliTesting]`
Learn [how to automate testing of CLIs](https://se-edu.github.io/se-book/testing/testAutomation/testingTextUis/).
#### Exercise: Practice automated CLI testing
* Run the tests as explained in the [Testing](#testing) section.
* Examine the test script to understand how the script works.
* Add a few more tests to the `input.txt`. Run the tests. It should fail.<br>
Modify `expected.txt` to make the tests pass again.
* Edit the `AddressBook.java` to modify the behavior slightly and modify tests to match.
## Use Collections `[LO-Collections]`
Note how the `AddressBook` class uses `ArrayList<>` class (from the Java `Collections` library) to store a list of `String` or `String[]` objects.
Learn [how to use some Java `Collections` classes, such as `ArrayList` and `HashMap`](https://se-edu.github.io/se-book/javaTools/collections/)
#### Exercise: Use `HashMap`
Currently, a person's details are stored as a `String[]`. Modify the code to use a `HashMap<String, String>` instead.
A sample code snippet is given below.
```java
private static final String PERSON_PROPERTY_NAME = "name";
private static final String PERSON_PROPERTY_EMAIL = "email";
...
HashMap<String,String> john = new HashMap<>();
john.put(PERSON_PROPERTY_NAME, "John Doe");
john.put(PERSON_PROPERTY_EMAIL, "john.doe@email.com");
//etc.
```
## Use Enums `[LO-Enums]`
#### Exercise: Use `HashMap` + `Enum`
Similar to the exercise in the `LO-Collections` section, but also bring in Java `enum` feature.
```java
private enum PersonProperty {NAME, EMAIL, PHONE};
...
HashMap<PersonProperty,String> john = new HashMap<>();
john.put(PersonProperty.NAME, "John Doe");
john.put(PersonProperty.EMAIL, "john.doe@email.com");
//etc.
```
## Use Varargs `[LO-Varargs]`
Note how the `showToUser` method uses [Java Varargs feature](https://se-edu.github.io/se-book/javaTools/varargs/).
#### Exercise: Use Varargs
Modify the code to remove the use of the Varargs feature.
Compare the code with and without the varargs feature.
## Follow a coding standard `[LO-CodingStandard]`
The given code follows the [coding standard][coding-standard]
for the most part.
This learning outcome is covered by the exercise in `[LO-Refactor]`.
## Apply coding best practices `[LO-CodingBestPractices]`
Most of the given code follows the best practices mentioned
[here][code-quality].
This learning outcome is covered by the exercise in `[LO-Refactor]`
## Refactor code `[LO-Refactor]`
**Resources**:
* [se-edu/se-gook: Refactoring](https://se-edu.github.io/se-book/refactoring/)
* [se-edu/se-book: Refactoring in Intellij](https://se-edu.github.io/se-book/intellij/refactoring/)
#### Exercise: Refactor the code to make it better
Note: this exercise covers two other Learning Outcomes: `[LO-CodingStandard]`, `[LO-CodingBestPractices]`
* Improve the code in the following ways,
* Fix [coding standard][coding-standard]
violations.
* Fix violations of the best practices given in [in this document][code-quality].
* Any other change that you think will improve the quality of the code.
* Try to do the modifications as a combination of standard refactorings given in this
[catalog](http://refactoring.com/catalog/)
* As far as possible, use automated refactoring features in IntelliJ.
* If you know how to use Git, commit code after each refactoring.<br>
In the commit message, mention which refactoring you applied.<br>
Example commit messages: `Extract variable isValidPerson`, `Inline method isValidPerson()`
* Remember to run the test script after each refactoring to prevent [regressions](https://se-edu.github.io/se-book/testing/testingTypes/regressionTesting).
## Abstract methods well `[LO-MethodAbstraction]`
Notice how most of the methods in `AddressBook` are short and focused (does only one thing and does it well).
**Case 1**. Consider the following three lines in the `main` method.
```java
String userCommand = getUserInput();
echoUserCommand(userCommand);
String feedback = executeCommand(userCommand);
```
If we include the code of `echoUserCommand(String)` method inside the `getUserInput()`
(resulting in the code given below), the behavior of AddressBook remains as before.
However, that is a not a good approach because now the `getUserInput()` is doing two distinct things.
A well-abstracted method should do only one thing.
```java
String userCommand = getUserInput(); //also echos the command back to the user
String feedback = executeCommand(userCommand);
```
**Case 2**. Consider the method `removePrefixSign(String s, String sign)`.
While it is short, there are some problems with how it has been abstracted.
1. It contains the term `sign` which is not a term used by the AddressBook vocabulary.
> **A method adds a new term to the vocabulary used to express the solution**.
> Therefore, it is not good when a method name contains terms that are not strictly necessary to express the
> solution (e.g. there is another term already used to express the same thing) or not in tune with the solution
> (e.g. it does not go well with the other terms already used).
2. Its implementation is not doing exactly what is advertised by the method name and the header comment.
For example, the code does not remove only prefixes; it removes `sign` from anywhere in the `s`.
3. The method can be _more general_ and _more independent_ from the rest of the code. For example,
the method below can do the same job, but is more general (works for any string, not just parameters)
and is more independent from the rest of the code (not specific to AddressBook)
```java
/**
* Removes prefix from the given fullString if prefix occurs at the start of the string.
*/
private static String removePrefix(String fullString, String prefix) { ... }
```
If needed, a more AddressBook-specific method that works on parameter strings only can be defined.
In that case, that method can make use of the more general method suggested above.
#### Exercise: Improve abstraction of method
Refactor the method `removePrefixSign` as suggested above.
## Follow SLAP `[LO-SLAP]`
Notice how most of the methods in `AddressBook` are written at a single
level of abstraction (_cf_ [se-edu/se-book:SLAP](https://se-edu.github.io/se-book/codeQuality/practices/slapHard/))
Here is an example:
```java
public static void main(String[] args) {
showWelcomeMessage();
processProgramArgs(args);
loadDataFromStorage();
while (true) {
userCommand = getUserInput();
echoUserCommand(userCommand);
String feedback = executeCommand(userCommand);
showResultToUser(feedback);
}
}
```
#### Exercise 1: Reduce SLAP of method
In the `main` method, replace the `processProgramArgs(args)` call with the actual code of that method.
The `main` method no longer has SLAP. Notice how mixing low level code with high level code reduces
readability.
#### Exercise 2: Refactor the code to make it worse!
Sometimes, going in the wrong direction can be a good learning experience too.
In this exercise, we explore how low code qualities can go.
* Refactor the code to make the code as bad as possible.<br>
i.e. How bad can you make it without breaking the functionality while still making it look like it was written by a
programmer (but a very bad programmer :-)).
* In particular, inlining methods can worsen the code quality fast.
## Work in a 1kLoC code base`[LO-1KLoC]`
#### Exercise: Enhance the code
Enhance the AddressBook to prove that you can work in a codebase of 1KLoC. <br>
Remember to change code in small steps, update/run tests after each change, and commit after each significant change.
Some suggested enhancements:
* Make the `find` command case insensitive e.g. `find john` should match `John`
* Add a `sort` command that can list the persons in alphabetical order
* Add an `edit` command that can edit properties of a specific person
* Add an additional field (like date of birth) to the person record
-----------------------------------------------------------------------------------------------------
# Contributors
The full list of contributors for se-edu can be found [here](https://se-edu.github.io/docs/Team.html).
-----------------------------------------------------------------------------------------------------
# Contact Us
* **Bug reports, Suggestions**: Post in our [issue tracker](https://github.com/se-edu/addressbook-level1/issues)
if you noticed bugs or have suggestions on how to improve.
* **Contributing**: We welcome pull requests. Refer to our website [here](https://se-edu.github.io/#contributing).
* If you would like to contact us, refer to [our website](https://se-edu.github.io/#contact).
[coding-standard]: https://github.com/oss-generic/process/blob/master/codingStandards/CodingStandard-Java.md "Java Coding Standard"
[code-quality]: https://se-edu.github.io/se-book/codeQuality/ "Code Quality Best Practices"
================================================
FILE: _config.yml
================================================
theme: jekyll-theme-cayman
================================================
FILE: src/seedu/addressbook/AddressBook.java
================================================
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.*;
/*
* 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-insensitive) 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 HashMap to store details of a single person.
* The enum type given below is the keys for different data elements of a person
* used by the internal HashMap<PersonProperty, String> storage format.
* For example, a person's name is stored as a key-value pair in a HashMap with key PersonProperty.NAME
*/
private enum PersonProperty {
NAME, EMAIL, PHONE
}
/**
* 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<HashMap<PersonProperty, String>>, as follows:
* private static final List<HashMap<PersonProperty, 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<HashMap<PersonProperty, 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<HashMap<PersonProperty, 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.
* ====================================================================
*/
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();
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<HashMap<PersonProperty, 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 HashMap<PersonProperty, 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(HashMap<PersonProperty, 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<HashMap<PersonProperty, 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<HashMap<PersonProperty, 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<HashMap<PersonProperty, String>> getPersonsWithNameContainingAnyKeyword(Collection<String> keywords) {
Set<String> keywordsInLowerCase = convertToLowerCase(keywords);
final ArrayList<HashMap<PersonProperty, String>> matchedPersons = new ArrayList<>();
for (HashMap<PersonProperty, String> person : getAllPersonsInAddressBook()) {
final Set<String> wordsInName = new HashSet<>(splitByWhitespace(getNameFromPerson(person)));
Set<String> wordsInNameInLowerCase = convertToLowerCase(wordsInName);
if (!Collections.disjoint(wordsInNameInLowerCase, keywordsInLowerCase)) {
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 HashMap<PersonProperty, 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(HashMap<PersonProperty, 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<HashMap<PersonProperty, 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<HashMap<PersonProperty, 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<HashMap<PersonProperty, String>> persons) {
final StringBuilder messageAccumulator = new StringBuilder();
for (int i = 0; i < persons.size(); i++) {
final HashMap<PersonProperty, 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, HashMap<PersonProperty, 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(HashMap<PersonProperty, 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<HashMap<PersonProperty, 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 HashMap<PersonProperty, 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<HashMap<PersonProperty, String>> loadPersonsFromFile(String filePath) {
final Optional<ArrayList<HashMap<PersonProperty, 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<HashMap<PersonProperty, 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(HashMap<PersonProperty, 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(HashMap<PersonProperty, String> exactPerson) {
final boolean isChanged = ALL_PERSONS.remove(exactPerson);
if (isChanged) {
savePersonsToFile(getAllPersonsInAddressBook(), storageFilePath);
}
return isChanged;
}
/**
* Returns all persons in the address book
*/
private static ArrayList<HashMap<PersonProperty, 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<HashMap<PersonProperty, 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(HashMap<PersonProperty, String> person) {
return person.get(PersonProperty.NAME);
}
/**
* Returns given person's phone number
*
* @param person whose phone number you want
*/
private static String getPhoneFromPerson(HashMap<PersonProperty, String> person) {
return person.get(PersonProperty.PHONE);
}
/**
* Returns given person's email
*
* @param person whose email you want
*/
private static String getEmailFromPerson(HashMap<PersonProperty, String> person) {
return person.get(PersonProperty.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 HashMap<PersonProperty, String> makePersonFromData(String name, String phone, String email) {
final HashMap<PersonProperty, String> person = new HashMap<PersonProperty, String>();
person.put(PersonProperty.NAME, name);
person.put(PersonProperty.PHONE, phone);
person.put(PersonProperty.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(HashMap<PersonProperty, 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<HashMap<PersonProperty, String>> persons) {
final ArrayList<String> encoded = new ArrayList<>();
for (HashMap<PersonProperty, 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<HashMap<PersonProperty, 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 HashMap<PersonProperty, 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<HashMap<PersonProperty, String>>> decodePersonsFromStrings(ArrayList<String> encodedPersons) {
final ArrayList<HashMap<PersonProperty, String>> decodedPersons = new ArrayList<>();
for (String encodedPerson : encodedPersons) {
final Optional<HashMap<PersonProperty, 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 removePrefix(encoded.substring(indexOfPhonePrefix, encoded.length()).trim(),
PERSON_DATA_PREFIX_PHONE);
// phone is middle arg, target is from own prefix to next prefix
} else {
return removePrefix(
encoded.substring(indexOfPhonePrefix, indexOfEmailPrefix).trim(),
PERSON_DATA_PREFIX_PHONE);
}
}
/**
* Extracts substring representing email from person string representation
*
* @param encoded person string representation
* @return email argument WITHOUT prefix
*/
private static String extractEmailFromPersonString(String encoded) {
final int indexOfPhonePrefix = encoded.indexOf(PERSON_DATA_PREFIX_PHONE);
final int indexOfEmailPrefix = encoded.indexOf(PERSON_DATA_PREFIX_EMAIL);
// email is last arg, target is from prefix to end of string
if (indexOfEmailPrefix > indexOfPhonePrefix) {
return removePrefix(encoded.substring(indexOfEmailPrefix, encoded.length()).trim(),
PERSON_DATA_PREFIX_EMAIL);
// email is middle arg, target is from own prefix to next prefix
} else {
return removePrefix(
encoded.substring(indexOfEmailPrefix, indexOfPhonePrefix).trim(),
PERSON_DATA_PREFIX_EMAIL);
}
}
/**
* Returns true if the given person's data fields are valid
*
* @param person HashMap representing the person (used in internal data)
*/
private static boolean isPersonDataValid(HashMap<PersonProperty, String> person) {
return isPersonNameValid(person.get(PersonProperty.NAME))
&& isPersonPhoneValid(person.get(PersonProperty.PHONE))
&& isPersonEmailValid(person.get(PersonProperty.EMAIL));
}
/*
* NOTE : =============================================================
* Note the use of 'regular expressions' in the method below.
* Regular expressions can be very useful in checking if a a string
* follows a specific format.
* ====================================================================
*/
/**
* Returns true if the given string as a legal person name
*
* @param name to be validated
*/
private static boolean isPersonNameValid(String name) {
return name.matches("(\\w|\\s)+"); // name is nonempty mixture of alphabets and whitespace
//TODO: implement a more permissive validation
}
/**
* Returns true if the given string as a legal person phone number
*
* @param phone to be validated
*/
private static boolean isPersonPhoneValid(String phone) {
return phone.matches("\\d+"); // phone nonempty sequence of digits
//TODO: implement a more permissive validation
}
/**
* Returns true if the given string is a legal person email
*
* @param email to be validated
* @return whether arg is a valid person email
*/
private static boolean isPersonEmailValid(String email) {
return email.matches("\\S+@\\S+\\.\\S+"); // email is [non-whitespace]@[non-whitespace].[non-whitespace]
//TODO: implement a more permissive validation
}
/*
* ===============================================
* COMMAND HELP INFO FOR USERS
* ===============================================
*/
/**
* Returns usage info for all commands
*/
private static String getUsageInfoForAllCommands() {
return getUsageInfoForAddCommand() + LS
+ getUsageInfoForFindCommand() + LS
+ getUsageInfoForViewCommand() + LS
+ getUsageInfoForDeleteCommand() + LS
+ getUsageInfoForClearCommand() + LS
+ getUsageInfoForExitCommand() + LS
+ getUsageInfoForHelpCommand();
}
/**
* Returns the string for showing 'add' command usage instruction
*/
private static String getUsageInfoForAddCommand() {
return String.format(MESSAGE_COMMAND_HELP, COMMAND_ADD_WORD, COMMAND_ADD_DESC) + LS
+ String.format(MESSAGE_COMMAND_HELP_PARAMETERS, COMMAND_ADD_PARAMETERS) + LS
+ String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_ADD_EXAMPLE) + LS;
}
/**
* Returns the string for showing 'find' command usage instruction
*/
private static String getUsageInfoForFindCommand() {
return String.format(MESSAGE_COMMAND_HELP, COMMAND_FIND_WORD, COMMAND_FIND_DESC) + LS
+ String.format(MESSAGE_COMMAND_HELP_PARAMETERS, COMMAND_FIND_PARAMETERS) + LS
+ String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_FIND_EXAMPLE) + LS;
}
/**
* Returns the string for showing 'delete' command usage instruction
*/
private static String getUsageInfoForDeleteCommand() {
return String.format(MESSAGE_COMMAND_HELP, COMMAND_DELETE_WORD, COMMAND_DELETE_DESC) + LS
+ String.format(MESSAGE_COMMAND_HELP_PARAMETERS, COMMAND_DELETE_PARAMETER) + LS
+ String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_DELETE_EXAMPLE) + LS;
}
/**
* Returns string for showing 'clear' command usage instruction
*/
private static String getUsageInfoForClearCommand() {
return String.format(MESSAGE_COMMAND_HELP, COMMAND_CLEAR_WORD, COMMAND_CLEAR_DESC) + LS
+ String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_CLEAR_EXAMPLE) + LS;
}
/**
* Returns the string for showing 'view' command usage instruction
*/
private static String getUsageInfoForViewCommand() {
return String.format(MESSAGE_COMMAND_HELP, COMMAND_LIST_WORD, COMMAND_LIST_DESC) + LS
+ String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_LIST_EXAMPLE) + LS;
}
/**
* Returns string for showing 'help' command usage instruction
*/
private static String getUsageInfoForHelpCommand() {
return String.format(MESSAGE_COMMAND_HELP, COMMAND_HELP_WORD, COMMAND_HELP_DESC)
+ String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_HELP_EXAMPLE);
}
/**
* Returns the string for showing 'exit' command usage instruction
*/
private static String getUsageInfoForExitCommand() {
return String.format(MESSAGE_COMMAND_HELP, COMMAND_EXIT_WORD, COMMAND_EXIT_DESC)
+ String.format(MESSAGE_COMMAND_HELP_EXAMPLE, COMMAND_EXIT_EXAMPLE);
}
/*
* ============================
* UTILITY METHODS
* ============================
*/
/**
* Removes prefix from the given fullString if prefix occurs at the start of the string.
*
* @param fullString Parameter as a string
* @param prefix Parameter sign to be removed
* @return string without the sign
*/
private static String removePrefix(String fullString, String prefix) {
return fullString.replaceFirst(prefix, "");
}
/**
* Splits a source string into the list of substrings that were separated by whitespace.
*
* @param toSplit source string
* @return split by whitespace
*/
private static ArrayList<String> splitByWhitespace(String toSplit) {
return new ArrayList<>(Arrays.asList(toSplit.trim().split("\\s+")));
}
/**
* Convert all strings in a Set<String> to lower case
*
* @param toConvert source collection
* @return collection with strings converted to lower case
*/
private static Set<String> convertToLowerCase(Collection<String> toConvert) {
Set<String> convertedStrings = new HashSet<>();
Iterator<String> ite = toConvert.iterator();
while (ite.hasNext()) {
convertedStrings.add(ite.next().toLowerCase());
}
return convertedStrings;
}
}
================================================
FILE: test/exitinput.txt
================================================
# exit directly
exit
================================================
FILE: test/expected.txt
================================================
][ ===================================================
][ ===================================================
][ AddessBook Level 1 - Version 1.0
][ Welcome to your Address Book!
][ ===================================================
][ The given file name [ ] is not a valid file name!
][ Exiting Address Book... Good bye!
][ ===================================================
][ ===================================================
][ ===================================================
][ ===================================================
][ AddessBook Level 1 - Version 1.0
][ Welcome to your Address Book!
][ ===================================================
][ The given file name [directoryThatDoesNotExist/valid.filename] is not a valid file name!
][ Exiting Address Book... Good bye!
][ ===================================================
][ ===================================================
][ ===================================================
][ ===================================================
][ AddessBook Level 1 - Version 1.0
][ Welcome to your Address Book!
][ ===================================================
][ The given file name [.noFilename] is not a valid file name!
][ Exiting Address Book... Good bye!
][ ===================================================
][ ===================================================
][ ===================================================
][ ===================================================
][ AddessBook Level 1 - Version 1.0
][ Welcome to your Address Book!
][ ===================================================
][ The given file name [data/notRegularFile.txt] is not a valid file name!
][ Exiting Address Book... Good bye!
][ ===================================================
][ ===================================================
][ ===================================================
][ ===================================================
][ AddessBook Level 1 - Version 1.0
][ Welcome to your Address Book!
][ ===================================================
][ Enter command: ][ [Command entered:exit]
][ Exiting Address Book... Good bye!
][ ===================================================
][ ===================================================
][ ===================================================
][ ===================================================
][ AddessBook Level 1 - Version 1.0
][ Welcome to your Address Book!
][ ===================================================
][ Using default storage file : addressbook.txt
][ Enter command: ][ [Command entered: sfdfd]
][ Invalid command format: sfdfd
][ add: Adds a person to the address book.
][ Parameters: NAME p/PHONE_NUMBER e/EMAIL
][ Example: add John Doe p/98765432 e/johnd@gmail.com
][
][ find: Finds all persons whose names contain any of the specified keywords (case-sensitive) and displays them as a list with index numbers.
][ Parameters: KEYWORD [MORE_KEYWORDS]
][ Example: find alice bob charlie
][
][ list: Displays all persons as a list with index numbers.
][ Example: list
][
][ delete: Deletes a person identified by the index number used in the last find/list call.
][ Parameters: INDEX
][ Example: delete 1
][
][ clear: Clears address book permanently.
][ Example: clear
][
][ exit: Exits the program. Example: exit
][ help: Shows program usage instructions. Example: help
][ ===================================================
][ Enter command: ][ [Command entered: clear]
][ Address book has been cleared!
][ ===================================================
][ Enter command: ][ [Command entered: list]
][
][ 0 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: add wrong args wrong args]
][ Invalid command format: add
][ add: Adds a person to the address book.
][ Parameters: NAME p/PHONE_NUMBER e/EMAIL
][ Example: add John Doe p/98765432 e/johnd@gmail.com
][
][ ===================================================
][ Enter command: ][ [Command entered: add Valid Name p/12345 valid@email.butNoPrefix]
][ Invalid command format: add
][ add: Adds a person to the address book.
][ Parameters: NAME p/PHONE_NUMBER e/EMAIL
][ Example: add John Doe p/98765432 e/johnd@gmail.com
][
][ ===================================================
][ Enter command: ][ [Command entered: add Valid Name 12345 e/valid@email.butPhonePrefixMissing]
][ Invalid command format: add
][ add: Adds a person to the address book.
][ Parameters: NAME p/PHONE_NUMBER e/EMAIL
][ Example: add John Doe p/98765432 e/johnd@gmail.com
][
][ ===================================================
][ Enter command: ][ [Command entered: add []\[;] p/12345 e/valid@e.mail]
][ Invalid command format: add
][ add: Adds a person to the address book.
][ Parameters: NAME p/PHONE_NUMBER e/EMAIL
][ Example: add John Doe p/98765432 e/johnd@gmail.com
][
][ ===================================================
][ Enter command: ][ [Command entered: add Valid Name p/not_numbers e/valid@e.mail]
][ Invalid command format: add
][ add: Adds a person to the address book.
][ Parameters: NAME p/PHONE_NUMBER e/EMAIL
][ Example: add John Doe p/98765432 e/johnd@gmail.com
][
][ ===================================================
][ Enter command: ][ [Command entered: add Valid Name p/12345 e/notAnEmail]
][ Invalid command format: add
][ add: Adds a person to the address book.
][ Parameters: NAME p/PHONE_NUMBER e/EMAIL
][ Example: add John Doe p/98765432 e/johnd@gmail.com
][
][ ===================================================
][ Enter command: ][ [Command entered: add Adam Brown p/111111 e/adam@gmail.com]
][ New person added: Adam Brown, Phone: 111111, Email: adam@gmail.com
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com
][
][ 1 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: add Betsy Choo p/222222 e/benchoo@nus.edu.sg]
][ New person added: Betsy Choo, Phone: 222222, Email: benchoo@nus.edu.sg
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com
][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][
][ 2 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: add Charlie Dickson e/charlie.d@nus.edu.sg p/333333]
][ New person added: Charlie Dickson, Phone: 333333, Email: charlie.d@nus.edu.sg
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com
][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][ 3. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg
][
][ 3 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: add Dickson Ee e/dickson@nus.edu.sg p/444444]
][ New person added: Dickson Ee, Phone: 444444, Email: dickson@nus.edu.sg
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com
][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][ 3. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg
][ 4. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg
][
][ 4 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: add Esther Potato p/555555 e/esther@notreal.potato]
][ New person added: Esther Potato, Phone: 555555, Email: esther@notreal.potato
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com
][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][ 3. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg
][ 4. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg
][ 5. Esther Potato Phone Number: 555555 Email: esther@notreal.potato
][
][ 5 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: find]
][
][ 0 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: find bet]
][
][ 0 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: find 23912039120]
][
][ 0 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: find betsy]
][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][
][ 1 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: find Betsy]
][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][
][ 1 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: find Dickson]
][ 1. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg
][ 2. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg
][
][ 2 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: find Charlie Betsy]
][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][ 2. Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg
][
][ 2 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: delete]
][ Invalid command format: delete
][ delete: Deletes a person identified by the index number used in the last find/list call.
][ Parameters: INDEX
][ Example: delete 1
][
][ ===================================================
][ Enter command: ][ [Command entered: delete should be only one number]
][ Invalid command format: delete
][ delete: Deletes a person identified by the index number used in the last find/list call.
][ Parameters: INDEX
][ Example: delete 1
][
][ ===================================================
][ Enter command: ][ [Command entered: delete -1]
][ Invalid command format: delete
][ delete: Deletes a person identified by the index number used in the last find/list call.
][ Parameters: INDEX
][ Example: delete 1
][
][ ===================================================
][ Enter command: ][ [Command entered: delete 0]
][ Invalid command format: delete
][ delete: Deletes a person identified by the index number used in the last find/list call.
][ Parameters: INDEX
][ Example: delete 1
][
][ ===================================================
][ Enter command: ][ [Command entered: delete 3]
][ The person index provided is invalid
][ ===================================================
][ Enter command: ][ [Command entered: delete 2]
][ Deleted Person: Charlie Dickson Phone Number: 333333 Email: charlie.d@nus.edu.sg
][ ===================================================
][ Enter command: ][ [Command entered: delete 2]
][ Person could not be found in address book
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com
][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][ 3. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg
][ 4. Esther Potato Phone Number: 555555 Email: esther@notreal.potato
][
][ 4 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: delete 4]
][ Deleted Person: Esther Potato Phone Number: 555555 Email: esther@notreal.potato
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Adam Brown Phone Number: 111111 Email: adam@gmail.com
][ 2. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][ 3. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg
][
][ 3 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: delete 1]
][ Deleted Person: Adam Brown Phone Number: 111111 Email: adam@gmail.com
][ ===================================================
][ Enter command: ][ [Command entered: list]
][ 1. Betsy Choo Phone Number: 222222 Email: benchoo@nus.edu.sg
][ 2. Dickson Ee Phone Number: 444444 Email: dickson@nus.edu.sg
][
][ 2 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: clear]
][ Address book has been cleared!
][ ===================================================
][ Enter command: ][ [Command entered: list]
][
][ 0 persons found!
][ ===================================================
][ Enter command: ][ [Command entered: exit]
][ Exiting Address Book... Good bye!
][ ===================================================
][ ===================================================
================================================
FILE: test/input.txt
================================================
##########################################################
# invalid command
##########################################################
# should recognise invalid command
sfdfd
##########################################################
# clean and check state
##########################################################
# address book should be emptied
clear
list
##########################################################
# test add person command, setup state for future tests
##########################################################
# should catch invalid args format
add wrong args wrong args
add Valid Name p/12345 valid@email.butNoPrefix
add Valid Name 12345 e/valid@email.butPhonePrefixMissing
# should catch invalid person data
add []\[;] p/12345 e/valid@e.mail
add Valid Name p/not_numbers e/valid@e.mail
add Valid Name p/12345 e/notAnEmail
# should add correctly
add Adam Brown p/111111 e/adam@gmail.com
list
add Betsy Choo p/222222 e/benchoo@nus.edu.sg
list
# order of phone and email should not matter
add Charlie Dickson e/charlie.d@nus.edu.sg p/333333
list
add Dickson Ee e/dickson@nus.edu.sg p/444444
list
add Esther Potato p/555555 e/esther@notreal.potato
list
##########################################################
# test find persons command
##########################################################
# should match none with no keywords
find
# should only match full words in person names
find bet
# does not match if none have keyword
find 23912039120
# matching should be case-insensitive
find betsy
# find unique keyword
find Betsy
# find multiple with same keyword
find Dickson
# find multiple with some keywords
find Charlie Betsy
##########################################################
# test delete person command
##########################################################
# last active view: [1] betsy [2] charlie
# should catch invalid args format
delete
delete should be only one number
# should catch invalid index
delete -1
delete 0
delete 3
# should catch attempt to delete something already deleted
delete 2
delete 2
# should have deleted based on last active view's index
list
# deletes correct person
delete 4
list
# listing indexes get updated on next request
delete 1
list
##########################################################
# test clear command
##########################################################
# clears all
clear
list
##########################################################
# test exit command
##########################################################
# exits properly
exit
list
================================================
FILE: test/runtests.bat
================================================
@ECHO OFF
REM create bin directory if it doesn't exist
if not exist ..\bin mkdir ..\bin
REM compile the code into the bin folder
javac ..\src\seedu\addressbook\Addressbook.java -d ..\bin
REM (invalid) no parent directory, invalid filename with no extension
java -classpath ..\bin seedu.addressbook.AddressBook " " < NUL > actual.txt
REM (invalid) invalid parent directory that does not exist, valid filename
java -classpath ..\bin seedu.addressbook.AddressBook "directoryThatDoesNotExist/valid.filename" < NUL >> actual.txt
REM (invalid) no parent directory, invalid filename with dot on first character
java -classpath ..\bin seedu.addressbook.AddressBook ".noFilename" < NUL >> actual.txt
REM (invalid) valid parent directory, non regular file
if not exist data\notRegularFile.txt mkdir data\notRegularFile.txt
java -classpath ..\bin seedu.addressbook.AddressBook "data/notRegularFile.txt" < NUL >> actual.txt
REM (valid) valid parent directory, valid filename with extension.
copy /y NUL data\valid.filename
java -classpath ..\bin seedu.addressbook.AddressBook "data/valid.filename" < exitinput.txt >> actual.txt
REM run the program, feed commands from input.txt file and redirect the output to the actual.txt
java -classpath ..\bin seedu.addressbook.AddressBook < input.txt >> actual.txt
REM compare the output to the expected output
FC actual.txt expected.txt
================================================
FILE: test/runtests.sh
================================================
#!/usr/bin/env bash
# change to script directory
cd "${0%/*}"
# create ../bin directory if not exists
if [ ! -d "../bin" ]
then
mkdir ../bin
fi
# compile the code into the bin folder
javac ../src/seedu/addressbook/AddressBook.java -d ../bin
# (invalid) no parent directory, invalid filename with no extension
java -classpath ../bin seedu.addressbook.AddressBook ' ' < /dev/null > actual.txt
# (invalid) invalid parent directory that does not exist, valid filename
java -classpath ../bin seedu.addressbook.AddressBook 'directoryThatDoesNotExist/valid.filename' < /dev/null >> actual.txt
# (invalid) no parent directory, invalid filename with dot on first character
java -classpath ../bin seedu.addressbook.AddressBook '.noFilename' < /dev/null >> actual.txt
# (invalid) valid parent directory, non regular file
mkdir -p data/notRegularFile.txt
java -classpath ../bin seedu.addressbook.AddressBook 'data/notRegularFile.txt' < /dev/null >> actual.txt
# (valid) valid parent directory, valid filename with extension.
touch data/valid.filename
java -classpath ../bin seedu.addressbook.AddressBook 'data/valid.filename' < exitinput.txt >> actual.txt
# run the program, feed commands from input.txt file and redirect the output to the actual.txt
java -classpath ../bin seedu.addressbook.AddressBook < input.txt >> actual.txt
# compare the output to the expected output
diff actual.txt expected.txt
if [ $? -eq 0 ]
then
echo "Test result: PASSED"
else
echo "Test result: FAILED"
fi
gitextract_xm6qqcx3/
├── .gitignore
├── LICENSE
├── README.md
├── _config.yml
├── src/
│ └── seedu/
│ └── addressbook/
│ └── AddressBook.java
└── test/
├── exitinput.txt
├── expected.txt
├── input.txt
├── runtests.bat
└── runtests.sh
SYMBOL INDEX (75 symbols across 1 files)
FILE: src/seedu/addressbook/AddressBook.java
class AddressBook (line 30) | public class AddressBook {
type PersonProperty (line 137) | private enum PersonProperty {
method main (line 202) | public static void main(String[] args) {
method showWelcomeMessage (line 222) | private static void showWelcomeMessage() {
method showResultToUser (line 226) | private static void showResultToUser(String result) {
method echoUserCommand (line 241) | private static void echoUserCommand(String userCommand) {
method processProgramArgs (line 252) | private static void processProgramArgs(String[] args) {
method setupGivenFileForStorage (line 272) | private static void setupGivenFileForStorage(String filePath) {
method exitProgram (line 286) | private static void exitProgram() {
method setupDefaultFileForStorage (line 296) | private static void setupDefaultFileForStorage() {
method isValidFilePath (line 307) | private static boolean isValidFilePath(String filePath) {
method hasValidParentDirectory (line 323) | private static boolean hasValidParentDirectory(Path filePath) {
method hasValidFileName (line 334) | private static boolean hasValidFileName(Path filePath) {
method loadDataFromStorage (line 343) | private static void loadDataFromStorage() {
method executeCommand (line 360) | private static String executeCommand(String userInputString) {
method splitCommandWordAndArgs (line 389) | private static String[] splitCommandWordAndArgs(String rawUserInput) {
method getMessageForInvalidCommandInput (line 400) | private static String getMessageForInvalidCommandInput(String userComm...
method executeAddPerson (line 411) | private static String executeAddPerson(String commandArgs) {
method getMessageForSuccessfulAddPerson (line 433) | private static String getMessageForSuccessfulAddPerson(HashMap<PersonP...
method executeFindPersons (line 445) | private static String executeFindPersons(String commandArgs) {
method getMessageForPersonsDisplayedSummary (line 458) | private static String getMessageForPersonsDisplayedSummary(ArrayList<H...
method extractKeywordsFromFindPersonArgs (line 468) | private static Set<String> extractKeywordsFromFindPersonArgs(String fi...
method getPersonsWithNameContainingAnyKeyword (line 478) | private static ArrayList<HashMap<PersonProperty, String>> getPersonsWi...
method executeDeletePerson (line 497) | private static String executeDeletePerson(String commandArgs) {
method isDeletePersonArgsValid (line 516) | private static boolean isDeletePersonArgsValid(String rawArgs) {
method extractTargetIndexFromDeletePersonArgs (line 531) | private static int extractTargetIndexFromDeletePersonArgs(String rawAr...
method isDisplayIndexValidForLastPersonListingView (line 541) | private static boolean isDisplayIndexValidForLastPersonListingView(int...
method getMessageForSuccessfulDelete (line 552) | private static String getMessageForSuccessfulDelete(HashMap<PersonProp...
method executeClearAddressBook (line 561) | private static String executeClearAddressBook() {
method executeListAllPersonsInAddressBook (line 571) | private static String executeListAllPersonsInAddressBook() {
method executeExitProgramRequest (line 580) | private static void executeExitProgramRequest() {
method getUserInput (line 596) | private static String getUserInput() {
method showToUser (line 616) | private static void showToUser(String... message) {
method showToUser (line 626) | private static void showToUser(ArrayList<HashMap<PersonProperty, Strin...
method getDisplayString (line 635) | private static String getDisplayString(ArrayList<HashMap<PersonPropert...
method getIndexedPersonListElementMessage (line 654) | private static String getIndexedPersonListElementMessage(int visibleIn...
method getMessageForFormattedPersonData (line 664) | private static String getMessageForFormattedPersonData(HashMap<PersonP...
method updateLatestViewedPersonListing (line 674) | private static void updateLatestViewedPersonListing(ArrayList<HashMap<...
method getPersonByLastVisibleIndex (line 685) | private static HashMap<PersonProperty, String> getPersonByLastVisibleI...
method createFileIfMissing (line 701) | private static void createFileIfMissing(String filePath) {
method loadPersonsFromFile (line 725) | private static ArrayList<HashMap<PersonProperty, String>> loadPersonsF...
method getLinesInFile (line 738) | private static ArrayList<String> getLinesInFile(String filePath) {
method savePersonsToFile (line 757) | private static void savePersonsToFile(ArrayList<HashMap<PersonProperty...
method addPersonToAddressBook (line 779) | private static void addPersonToAddressBook(HashMap<PersonProperty, Str...
method deletePersonFromAddressBook (line 790) | private static boolean deletePersonFromAddressBook(HashMap<PersonPrope...
method getAllPersonsInAddressBook (line 801) | private static ArrayList<HashMap<PersonProperty, String>> getAllPerson...
method clearAddressBook (line 808) | private static void clearAddressBook() {
method initialiseAddressBookModel (line 818) | private static void initialiseAddressBookModel(ArrayList<HashMap<Perso...
method getNameFromPerson (line 835) | private static String getNameFromPerson(HashMap<PersonProperty, String...
method getPhoneFromPerson (line 844) | private static String getPhoneFromPerson(HashMap<PersonProperty, Strin...
method getEmailFromPerson (line 853) | private static String getEmailFromPerson(HashMap<PersonProperty, Strin...
method makePersonFromData (line 865) | private static HashMap<PersonProperty, String> makePersonFromData(Stri...
method encodePersonToString (line 879) | private static String encodePersonToString(HashMap<PersonProperty, Str...
method encodePersonsToStrings (line 890) | private static ArrayList<String> encodePersonsToStrings(ArrayList<Hash...
method decodePersonFromString (line 912) | private static Optional<HashMap<PersonProperty, String>> decodePersonF...
method decodePersonsFromStrings (line 933) | private static Optional<ArrayList<HashMap<PersonProperty, String>>> de...
method isPersonDataExtractableFrom (line 951) | private static boolean isPersonDataExtractableFrom(String personData) {
method extractNameFromPersonString (line 966) | private static String extractNameFromPersonString(String encoded) {
method extractPhoneFromPersonString (line 980) | private static String extractPhoneFromPersonString(String encoded) {
method extractEmailFromPersonString (line 1003) | private static String extractEmailFromPersonString(String encoded) {
method isPersonDataValid (line 1025) | private static boolean isPersonDataValid(HashMap<PersonProperty, Strin...
method isPersonNameValid (line 1044) | private static boolean isPersonNameValid(String name) {
method isPersonPhoneValid (line 1054) | private static boolean isPersonPhoneValid(String phone) {
method isPersonEmailValid (line 1065) | private static boolean isPersonEmailValid(String email) {
method getUsageInfoForAllCommands (line 1080) | private static String getUsageInfoForAllCommands() {
method getUsageInfoForAddCommand (line 1093) | private static String getUsageInfoForAddCommand() {
method getUsageInfoForFindCommand (line 1102) | private static String getUsageInfoForFindCommand() {
method getUsageInfoForDeleteCommand (line 1111) | private static String getUsageInfoForDeleteCommand() {
method getUsageInfoForClearCommand (line 1120) | private static String getUsageInfoForClearCommand() {
method getUsageInfoForViewCommand (line 1128) | private static String getUsageInfoForViewCommand() {
method getUsageInfoForHelpCommand (line 1136) | private static String getUsageInfoForHelpCommand() {
method getUsageInfoForExitCommand (line 1144) | private static String getUsageInfoForExitCommand() {
method removePrefix (line 1163) | private static String removePrefix(String fullString, String prefix) {
method splitByWhitespace (line 1173) | private static ArrayList<String> splitByWhitespace(String toSplit) {
method convertToLowerCase (line 1183) | private static Set<String> convertToLowerCase(Collection<String> toCon...
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (91K chars).
[
{
"path": ".gitignore",
"chars": 380,
"preview": "# Default storage file for addressbook : don't need to cleanup when running from IDE\naddressbook.txt\n\n# Compiled classfi"
},
{
"path": "LICENSE",
"chars": 1074,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 nus-oss\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.md",
"chars": 19620,
"preview": "# AddressBook (Level 1)\n* This is a CLI (Command Line Interface) Address Book application **written in procedural fashio"
},
{
"path": "_config.yml",
"chars": 26,
"preview": "theme: jekyll-theme-cayman"
},
{
"path": "src/seedu/addressbook/AddressBook.java",
"chars": 48664,
"preview": "package seedu.addressbook;\n\n/*\n * NOTE : =============================================================\n * This class is "
},
{
"path": "test/exitinput.txt",
"chars": 20,
"preview": "# exit directly\nexit"
},
{
"path": "test/expected.txt",
"chars": 13054,
"preview": "][ ===================================================\n][ ===================================================\n][ AddessB"
},
{
"path": "test/input.txt",
"chars": 2698,
"preview": "##########################################################\n# invalid command\n###########################################"
},
{
"path": "test/runtests.bat",
"chars": 1369,
"preview": "@ECHO OFF\n\nREM create bin directory if it doesn't exist\nif not exist ..\\bin mkdir ..\\bin\n\nREM compile the code into the "
},
{
"path": "test/runtests.sh",
"chars": 1492,
"preview": "#!/usr/bin/env bash\n\n# change to script directory\ncd \"${0%/*}\"\n\n# create ../bin directory if not exists\nif [ ! -d \"../bi"
}
]
About this extraction
This page contains the full source code of the kokonguyen191/addressbook-level1 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (86.3 KB), approximately 19.3k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.