This is my portfolio page primarily for showcasing my contributions to the project, Rolodex.

This is not my full portfolio. To view my full portfolio, visit https://zy-ang.github.io/Portfolio.

1. Project: Rolodex

Rolodex is a desktop contact management application for users who prefer working without a mouse to manage their contacts more efficiently. If you are a businessman with a list of clients to remember, a teacher who wants to organize their student information, or anyone with a need for contact management, Rolodex provides you with a way to organize your important contacts in a fast and productive manner.

The user interacts with the application using a CLI (Command Line Interface), and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC.

Code contributed:

1.1. Enhancement Added: Find by close words (fuzzy searching) and find by tags

1.1.1. For the User


find, filter, search or f: Locating Persons in Rolodex (Since v1.4)

Finds persons in Rolodex.
Format: find KEYWORD [MORE_KEYWORDS] [SORT_ARGUMENTS]

Press Tab after typing find to generate the required field(s). See 3.2. Auto-completion for more details.

Alternatives: find can be replaced by filter, search or f
Keyboard shortcut: Ctrl+F

  • Only the name and tags are searched.

  • The name is searched fuzzily.

  • The tags is searched exactly.

  • The search will only find distinct persons. e.g. han solo han solo will only return han solo

  • The search is case insensitive for names. e.g. hans will match Hans

  • The search is case sensitive for tags. e.g. only School will match School

  • The search is fuzzy for name words of 4 or more characters. e.g Hnas will match Hans

  • The search is exact for name words of less than 4 characters. e.g. hans will not match Han but han will match Han

  • The search is an OR search (i.e. Persons matching at least one keyword will be returned). e.g. Hans Bo will return Hans Gruber, Bo Yang

  • The order of the keywords does not matter. e.g. Hans school Bo will match Bo Hans and others with the school tag

Examples:

  • find John or find jhon
    Returns john and John Doe

  • search Betsy Tim John
    Returns any person having names Betsy, Tim, or John

  • find School
    Returns any person having tag School

  • find School werk
    Returns any person having tag School or tag werk

  • [SORT_ARGUMENTS] can be none, some, any or all of n/ p/ e/ a/ n/desc p/desc e/desc a/desc n/asc p/asc e/asc a/asc, delimited by spaces, in no particular fixed order.

  • The sort argument prefix for sorting by name is n/.

  • The sort argument prefix for sorting by phone is p/.

  • The sort argument prefix for sorting by email is e/.

  • The sort argument prefix for sorting by address is a/.

  • The sort argument postfix desc denotes that the sort field is to be sorted in descending lexicographical order.

  • The sort argument postfix asc denotes that the sort field is to be sorted in ascending lexicographical order.

  • Sort arguments without a postfix are sorted by ascending lexicographical order.

  • The order of the SORT_ARGUMENTS are left-prioritized.

Sort arguments do not count as search arguments.

Examples:

  • find John p/ or find jhon p/asc
    Returns john and John Doe, sorted by ascending phone number.

  • search Betsy Tim John n/ p/desc
    Returns any person having names Betsy, Tim, or John, sorted by name then by descending phone if names are equal.

  • find School a/desc
    Returns any person having tag School, sorted by descending address.

  • find School werk e/
    Returns any person having tag School or tag werk, sorted by email.

  • find e/ p/desc
    Returns an error (do not count as search arguments).


1.1.2. Why would we need this?

Users may sometimes key commands too quickly or forget the names of their contacts. This enhancement allows them to find contacts by close name words or by tags previously assigned to make finding contacts in the Rolodex a lot easier.

1.1.3. Implementation


Fuzzy Finding Mechanism

The fuzzy finding mechanism is powered by Levenshtein distance, a string heuristic for measuring the difference between two character sequences. It can be informally viewed as the number of hops that are required to get from a string to another, where hops are insertions, deletions and substitution operations.

The activity diagram for the fuzzy finding can be loosely described as seen in the figure below:

FuzzyFindActivityDiagram
  1. A user sends a find request

  2. Application looks at all contacts currently in the database

  3. If the search parameters loosely match any of the name words of the current contact, add it into the list view.

  4. Otherwise, if it is an exact match on any of the name words of the current contact, add it into the list view.

  5. Otherwise, if it matches any other conditions specified under the find command, add it into the list view.

If a contact’s name word has a shorter length than the specified global minimum, the condition automatically triggers to false and starts searching for exact matches.

The settings for the loose matching can be found as a static constant of the Person class.

public class Person implements ReadOnlyPerson {
    private static final int FIND_NAME_GLOBAL_TOLERANCE = 4;
    private static final int FIND_NAME_DISTANCE_TOLERANCE = 2;
    ...
}

FIND_NAME_GLOBAL_TOLERANCE is the global minimum distance of the currently examined name word for the loose matching to execute. FIND_NAME_DISTANCE_TOLERANCE is the maximum levenshtein distance from the currently examined search parameter to the currently examined name word in which the contact will be added.

Design Considerations
Length of name word

Alternative 1 (current choice): Use a global minimum for defining when the fuzzy finding should execute.
Pros: Name words have higher minimum hop to length ratio.
Cons: User might need fuzzy finding on name words with character length lesser than the global minimum.
Pros: Easy logic to understand, implement and maintain.
Cons: Rudimentary algorithm.


Location of fuzzy find settings

Alternative 1 (current choice): Use private static constant in a person.
Pros: User does not need to worry about changing the settings or the intricacies of the fuzzy finding.
Cons: User might need stricter or looser limits than the ones defined for them.
Pros: Developers can easily change and manage settings and limits.
Cons: Users have no way of accessing the settings and limits.

Alternative 2: Allow user to set fuzzy find settings in a document file.
Pros: Advanced users can change the settings to suit their needs better.
Cons: Developers need to spend more work maintaining more components including storage.


1.2. Enhancement Added: Sort

1.2.1. For the User


list, show, display or l: Listing All Persons (Since v1.3)

Shows a list of all persons in Rolodex, sorted by the specified sort order or default sort order.
Format: list [SORT_ARGUMENTS]
Alternatives: list can be replaced by show, display or l
Keyboard shortcut: Ctrl+L

  • [SORT_ARGUMENTS] can be none, some, any or all of n/ p/ e/ a/ n/desc p/desc e/desc a/desc n/asc p/asc e/asc a/asc, delimited by spaces, in no particular fixed order.

  • The sort argument prefix for sorting by name is n/.

  • The sort argument prefix for sorting by phone is p/.

  • The sort argument prefix for sorting by email is e/.

  • The sort argument prefix for sorting by address is a/.

  • The sort argument postfix desc denotes that the sort field is to be sorted in descending lexicographical order.

  • The sort argument postfix asc denotes that the sort field is to be sorted in ascending lexicographical order.

  • Sort arguments without a postfix are sorted by ascending lexicographical order.

  • The order of the SORT_ARGUMENTS are left-prioritized.

Examples:

  • list or l displays all persons by the default sort order.

  • l n/desc displays all persons sorted by descending name.

  • list p/ a/desc or list p/asc a/desc displays all persons sorted by ascending phone, then by descending address.

find, filter, search or f: Locating Persons in Rolodex (Since v1.4)

Finds persons in Rolodex.
Format: find KEYWORD [MORE_KEYWORDS] [SORT_ARGUMENTS]

Press Tab after typing find to generate the required field(s). See 3.2. Auto-completion for more details.

Alternatives: find can be replaced by filter, search or f
Keyboard shortcut: Ctrl+F

  • Only the name and tags are searched.

  • The name is searched fuzzily.

  • The tags is searched exactly.

  • The search will only find distinct persons. e.g. han solo han solo will only return han solo

  • The search is case insensitive for names. e.g. hans will match Hans

  • The search is case sensitive for tags. e.g. only School will match School

  • The search is fuzzy for name words of 4 or more characters. e.g Hnas will match Hans

  • The search is exact for name words of less than 4 characters. e.g. hans will not match Han but han will match Han

  • The search is an OR search (i.e. Persons matching at least one keyword will be returned). e.g. Hans Bo will return Hans Gruber, Bo Yang

  • The order of the keywords does not matter. e.g. Hans school Bo will match Bo Hans and others with the school tag

Examples:

  • find John or find jhon
    Returns john and John Doe

  • search Betsy Tim John
    Returns any person having names Betsy, Tim, or John

  • find School
    Returns any person having tag School

  • find School werk
    Returns any person having tag School or tag werk

  • [SORT_ARGUMENTS] can be none, some, any or all of n/ p/ e/ a/ n/desc p/desc e/desc a/desc n/asc p/asc e/asc a/asc, delimited by spaces, in no particular fixed order.

  • The sort argument prefix for sorting by name is n/.

  • The sort argument prefix for sorting by phone is p/.

  • The sort argument prefix for sorting by email is e/.

  • The sort argument prefix for sorting by address is a/.

  • The sort argument postfix desc denotes that the sort field is to be sorted in descending lexicographical order.

  • The sort argument postfix asc denotes that the sort field is to be sorted in ascending lexicographical order.

  • Sort arguments without a postfix are sorted by ascending lexicographical order.

  • The order of the SORT_ARGUMENTS are left-prioritized.

Sort arguments do not count as search arguments.

Examples:

  • find John p/ or find jhon p/asc
    Returns john and John Doe, sorted by ascending phone number.

  • search Betsy Tim John n/ p/desc
    Returns any person having names Betsy, Tim, or John, sorted by name then by descending phone if names are equal.

  • find School a/desc
    Returns any person having tag School, sorted by descending address.

  • find School werk e/
    Returns any person having tag School or tag werk, sorted by email.

  • find e/ p/desc
    Returns an error (do not count as search arguments).


1.2.2. Why would we need this?

Users may sometimes have too many contacts in a listing. This enhancement allows them to find contacts quickly by sorting the displayed list of persons. Users can operate on a sorted list for longer displayed lists via list or find command.

1.2.3. Implementation


Sorting Mechanism

The sorting mechanism is integrated into the list and find commands which resides in the application’s logic component. It supports sorting in ascending or descending order of most fields of a person (e.g. Name, Phone). The sort commands update the state of the application at the model level, which resides in ModelManager, via the method updateSortComparator.

The activity diagram for a sort can be loosely described as seen in the figure below:

SortingActivityDiagram
Sort arguments are case-sensitive.

The case for a list command:

  1. A user executes a list command with sort arguments.

  2. The application checks for invalid sort arguments.

  3. The application model creates a comparator using the sort arguments, in order.

  4. The application displays the results in the specified sort order.

The case for a find command:

  1. A user executes a find command.

  2. The application separates the command arguments into find data arguments and sort arguments.

  3. The application executes a search on the find arguments.

  4. The application model creates a comparator using the sort arguments, in order.

  5. The application displays the filtered results in the specified sort order.

Find cannot work on name words matching sort arguments.

The list command’s execute() implementation before sorting implementation:

@Override
public CommandResult execute() {
    model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
    return new CommandResult(MESSAGE_SUCCESS);
}

The list command’s execute() implementation after sorting implementation:

@Override
public CommandResult execute() {
    model.updateSortComparator(sortArguments);
    model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
    return new CommandResult(MESSAGE_SUCCESS);
}

Notice that model.updateSortComparator(sortArguments) is added to the command logic. The update will give the model a comparator before executing a sort and filter which is handled by the event consumer.

Design Considerations
Integrating sort

Alternative 1 (current choice): Integrate sort into existing commands.
Pros: User can quickly sort and list or find the data as specified without executing another separate command.
Cons: User might need to use the sort arguments for other purposes (e.g. find n/ does not return anyone matching n/).
Pros: Adds onto existing functionality and reduces clutter and unnecessary creation of a new command.
Cons: Bad SLAP on a user level.

Alternative 2: Create a new sort command.
Pros: User can decide when to sort the current list view
Cons: User needs an additional step before sorting a list.


Resetting sort comparator on add commands

Alternative 1 (current choice): Reset the current displayed list on an add command.
Pros: The displayed list always complies with the model comparator.
Cons: User might need to continue operating on the previous listing.

Alternative 2: Append added person to the current list view.
Pros: Users can immediately edit the person using the last listing index.
Cons: The displayed listing will not comply with the model comparator.


1.3. Enhancement Added: Open existing file and Create new file commands

1.3.1. For the User


open, o, cd, ls or <: Opening an Existing Rolodex Storage File (Since v1.3)

Reloads the application with data from a rolodex at the specified filepath.
Format: open FILEPATH or o FILEPATH
Keyboard Shortcut: Ctrl+O

  • Loads the rolodex at the specified FILEPATH.

  • The FILEPATH can be project-relative or any directory on the system.

  • The FILEPATH must be a file with xml formatted data.

  • The FILEPATH must end with a file extension .rldx in order for data to be saved.

  • If FILEPATH does not exist, use the new command instead.

  • The command recognizes directories in formats / or \ as valid filepaths

When the application is opened, Rolodex will load the last accessed Rolodex directory.

After opening the Rolodex, verify that you have correctly opened the Rolodex at the correct directory by checking the status bar of your application:

OpenNewStatusBar

Opening a file without a .rldx extension will not save your changes.

The open command will refresh your session and all existing undoable commands will be cleared. It is recommended that you confirm you are satisfied with your changes before opening a new file.

Examples:

  • open C:/Users/Rolodex/downloads/myOwn.rldx
    o C:/Users/Rolodex/downloads/myOwn.rldx
    o C:\Users\Rolodex\downloads\myOwn.rldx
    Loads the application with the data at the directory C:/Users/Rolodex/downloads/myOwn.rldx.

  • open data/default.rldx
    open data\default.rldx
    Loads the application with the data in default.rldx in the folder data.

new, n, touch or >: Creating a New Rolodex Storage File (Since v1.3)

Creates a new Rolodex at the specified filepath and reloads the application with new sample data.
Format: new FILEPATH or n FILEPATH
Keyboard Shortcut: Ctrl+N

  • Creates the rolodex at the specified FILEPATH.

  • The FILEPATH can be project-relative or any directory on the system.

  • The FILEPATH must end with a file extension .rldx in order for data to be created and saved.

  • If FILEPATH already exists, use the open command instead.

  • The command recognizes directories in formats / or \ as valid filepaths

After creating a new Rolodex, verify that you have correctly created the Rolodex at the correct directory by checking the status bar of your application:

OpenNewStatusBar

The new command will refresh your session and all existing undoable commands will be cleared. It is recommended that you confirm you are satisfied with your changes before opening a new file.

Examples:

  • new C:/Users/Rolodex/downloads/myOwn.rldx
    n C:/Users/Rolodex/downloads/myOwn.rldx
    n C:\Users\Rolodex\downloads\myOwn.rldx
    Creates a new Rolodex file myOwn.rldx at the directory C:\Users\Rolodex\downloads and reloads the application with sample data created for the new Rolodex.

  • new data/default.rldx
    new data\default.rldx
    Creates a new Rolodex file default.rldx at the relative directory data and reloads the application with sample data created for the new Rolodex.


1.3.2. Why would we need this?

Users may want to work on a different group of data for different purposes. Users with too many contacts can also create a separate database for dividing up the contacts into smaller, more manageable sizes.

1.3.3. Implementation


Open File and New File Mechanism

The open new file and creating new file mechanism are handled by two new types of requests under the EventsCenter, OpenRolodexRequestEvent and RolodexChangedDirectoryEvent. Upon receiving a new command, the MainApp attempts to reload the instance model and logic but not the UI.

The high level sequence diagram for a typical open request can be seen below:

OpenNewHighLevelSequenceDiagram
  1. A user requests to open data/default.rldx.

  2. The command is handled the same way as other commands, by the parser in logic, then via the OpenRolodexCommand object itself, under the Logic component of the application.

  3. Upon a successful parsing of the command and execution, the command would raise a new OpenRolodexRequestEvent to be handled by the EventsCenter.

  4. The EventsCenter posts a new OpenRolodexRequestEvent back to the MainApp where the new Rolodex path would be loaded with the successfully read data from the file path specified:

    • The preferences file of the user specified under config.json would now point to the latest active directory.

    • The storage, model and logic instances of the MainApp would be now loaded with data from the new active directory.

The behaviour of the save operation remains unchanged, to be handled under the RolodexChangedEvent. The Rolodex only writes to the active database upon an editor command (e.g. add, edit). The UI however, is updated as seen in the following sequence diagram:

OpenNewRolodexChangedLocationEventSequenceDiagram
  1. Upon successful handling of a OpenRolodexRequestEvent, MainApp raises a new RolodexChangedDirectoryEvent.

  2. The event is then reposted by the EventsCenter.

  3. The MainWindow of the UI instance consumes the event and updates the status bar with the new active filepath.

The New command only differs from the sequence of the open new Rolodex by being executed when no valid file exists at the specified active directory:

@Override
public CommandResult execute() throws CommandException {
    if (new File(filePath).exists()) {
        throw  new CommandException(String.format(MESSAGE_ALREADY_EXISTS, filePath));
    } else {
        EventsCenter.getInstance().post(new OpenRolodexRequestEvent(filePath));
        return new CommandResult(String.format(MESSAGE_CREATING, filePath));
    }
}

In contrast, the Open command is a reverse of the above:

@Override
public CommandResult execute() throws CommandException {
    if (new File(filePath).exists()) {
        EventsCenter.getInstance().post(new OpenRolodexRequestEvent(filePath));
        return new CommandResult(String.format(MESSAGE_OPENING, filePath));
    } else {
        throw new CommandException(String.format(MESSAGE_NOT_EXIST, filePath));
    }
}
The XmlRolodexStorage.java now only saves upon recognizing that the current file has a .rldx extension, which is a subset of xml for the Rolodex application, as seen in the snippet below:
public void saveRolodex(ReadOnlyRolodex rolodex, String filePath) throws IOException {
    ...
    if (!isValidRolodexStorageExtension(filePath)) {
        throw new InvalidExtensionException(
                String.format(MESSAGE_INVALID_EXTENSION_FORMAT, ROLODEX_FILE_EXTENSION));
    }
    ...
}
Design Considerations
General Implementation

Alternative 1 (current choice): Reload the current MainApp with the new database.
Pros: The application can be quickly reloaded onto the active window.
Cons: User might need to switch back and forth between the Rolodex directories.

Alternative 2: Reload MainApp with a new application instance.
Constraint: JavaFX only allows launch(args) to be launched once per execution time on a code level. Another approach would be to reload the application with a loaded and built .jar.
Pros: Easier logic to understand for new developers.
Cons: Building and debugging with a second .jar will be confusing and problematic to any development workflow.

Alternative 3: Open a new window with the loaded data.
Pros: The user can continue to operate on the previous Rolodex application.
Cons: The developer would have to spend time configuring different EventsCenter posts for the different application windows.


Saving only to files with .rldx extensions

Alternative 1 (current choice): Write to the active directory only if it has a .rldx extension.
Pros: The application control becomes stricter, preventing users from writing to a file with say a .png extension.
Cons: Extensions become forced even for valid databases with .xml file types.

Alternative 2: Allow writable to all file extensions.
Pros: Users can have more freedom in choice of file naming.
Cons: If the user accidentally writes to a file that is not a valid database, the user would corrupt the file data.


Separating open and new commands

Alternative 1 (current choice): Two commands, one for opening existing files and one for creating new files.
Pros: The user can easily be notified if a file exists or if it has been moved.
Cons: The user would have to take note of two commands instead of one.


1.4. Enhancement Improved: java.util.Set implementation for command abbreviations

1.4.1. Why would we need this?

Using constants to manually check if a command word belongs to a particular command is extremely tedious for both developers and users. In addition, using constants forces the program to have to check through all possible command abbreviations belonging to a command word in the worst case. A java HashSet takes care of these problems by improving access time and improves manageability of the codebase.

1.4.2. Implementation


Abbreviation Mechanism

The ability for users to enter a variety of different words for a same command is implemented using java.util.set.

An example as implemented in the open command:

import java.util.HashSet;
import java.util.Set;

public class OpenRolodexCommand extends Command {

    public static final String COMMAND_WORD = "open";
    public static final Set<String> COMMAND_WORD_ABBREVIATIONS =
            new HashSet<>(Arrays.asList(COMMAND_WORD, "o", "cd", "ls", "<"));
    ...
}

COMMAND_WORD_ABBREVIATIONS is implemented using a java.util.HashSet which offers reasonably fast access times.

The user would be able to use a wide variety of different commands such as o, cd like on Windows cmd, ls on UNIX-like systems or the loose < such as when you run a java program < input > output on a wide variety of systems.

It can easily be seen that using a set implementation, the developer would have an extremely easy time adding/ deleting new abbreviations. A set implementation also allows an easy way of testing if there are conflicting abbreviations, by simply permuting the COMMAND_WORD_ABBREVIATIONS property of all possible commands and checking if all permutations are pairwise disjoint:

@Test
public void parseAllCommandAbbreviationsAreDisjoint() {
    ArrayList<Pair<Set<String>, Set<String>>> commandAbbreviationPermutations =
            generateCommandAbbreviationPermutations(POSSIBLE_COMMAND_ABBREVIATIONS);
    for (Pair<Set<String>, Set<String>> commandAbbreviationPair : commandAbbreviationPermutations) {
        assertTrue(Collections.disjoint(commandAbbreviationPair.getKey(), commandAbbreviationPair.getValue()));
    }
}
Design Considerations
Implementation

Alternative 1 (current choice): A Set of strings
Pros: Constant access time per command level. Improves performance and reliability significantly.
Pros: Ease of testing for conflicting abbreviations, particularly where developers forget if they used a same abbreviation for two different commands (e.g. assigning h to both the history and help commands).
Cons: If you have a better suggestion than the above implementation, feel free to create a new issue for discussion on github.

Alternative 2 (previous choice): Manually named string Constants
Pros: Forces developer to be more prudent in checking through the conflicting abbreviations.
Cons: Developers would have to manually check through all commands to ensure the accuracy of the abbreviations, which is time-consuming and makes way for human errors.
Cons: A switch-case for all possible commands abbreviations would perform significantly slower than direct memory access on a java HashSet, in the worst case.


1.5. Enhancement Added: Search Highlighting in HelpWindow

1.5.1. Why would we need this?

JavaFx’s native WebView does not allow the user to easily search a html document, putting it on a subpar standard to modern web browsers. Implementing a search feature in the help window will allow the user to search for terms in the user guide with ease.

1.5.2. Implementation


Help Window Highlighting

Rolodex uses JavaFx version 8.0.111, where WebView does not support search functionality that mimics Ctrl + F capability of modern web browsers. As a workaround, Johann Burkard’s jQuery text highlighting library is being used to implement such a feature.

Users can easily find keywords by using the highlighting feature of the help window, as opposed to manually looking through the help document for information, which can be counterproductive:

Highlight

The discussion for JavaFX to implement a native search feature within WebView is ongoing and you can find it here.

Removing jQuery implementation

In a future version of JavaFx where such an implementation is no longer necessary, you may revert the changes by reverting pull request #99 on the Rolodex repo on GitHub. You may also choose to manually do this by reverting the following code:

Original HelpWindow.fxml:

<StackPane fx:id="helpWindowRoot" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
    <WebView fx:id="browser" />
</StackPane>

HelpWindow.fxml with makeshift user guide search functionality:

<StackPane fx:id="helpWindowRoot" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
    <VBox fx:id="layout">
        <children>
            <TextField fx:id="txtSearch" promptText="Search and press ENTER" />
                <HBox fx:id="controls">
                    <children>
                        <Button fx:id="btnSearch" mnemonicParsing="false" text="Search">
                            <HBox.margin>
                                <Insets right="10.0" />
                            </HBox.margin>
                        </Button>
                        <Button fx:id="btnClear" mnemonicParsing="false" text="Clear" />
                    </children>
                </HBox>
            <WebView fx:id="browser" minHeight="-Infinity" minWidth="-Infinity" prefHeight="100000.0" />
        </children>
    </VBox>
</StackPane>

Original HelpWindow.java:

@FXML
private WebView browser;

private final Stage dialogStage;

public HelpWindow() {
    super(FXML);
    Scene scene = new Scene(getRoot());
    //Null passed as the parent stage to make it non-modal.
    dialogStage = createDialogStage(TITLE, null, scene);
    dialogStage.setMaximized(true); //TODO: set a more appropriate initial size
    FxViewUtil.setStageIcon(dialogStage, ICON);

    String userGuideUrl = getClass().getResource(HELP_FILE_PATH).toString();
    browser.getEngine().load(userGuideUrl);
}

HelpWindow.java with makeshift user guide search functionality:

@FXML
private WebView browser;

@FXML
private TextField txtSearch;

@FXML
private Button btnSearch;

@FXML
private Button btnClear;

@FXML
private HBox controls;

private final Stage dialogStage;

public HelpWindow() {
    super(FXML);

    txtSearch.setOnAction(event -> {
        if (browser.getEngine().getDocument() != null) {
            highlight(browser.getEngine(), txtSearch.getText());
        }
    });

    btnSearch.setDefaultButton(true);
    btnSearch.setOnAction(actionEvent -> txtSearch.fireEvent(new ActionEvent()));

    btnClear.setOnAction(actionEvent -> clearHighlights(browser.getEngine()));
    btnClear.setCancelButton(true);

    controls.disableProperty().bind(browser.getEngine().getLoadWorker().runningProperty());
    txtSearch.disableProperty().bind(browser.getEngine().getLoadWorker().runningProperty());

    Scene scene = new Scene(getRoot());
    //Null passed as the parent stage to make it non-modal.
    dialogStage = createDialogStage(TITLE, null, scene);
    dialogStage.setMaximized(true); //TODO: set a more appropriate initial size
    FxViewUtil.setStageIcon(dialogStage, ICON);

    String userGuideUrl = getClass().getResource(HELP_FILE_PATH).toString();
    browser.getEngine().load(userGuideUrl);
}

private void highlight(WebEngine engine, String text) {
    engine.executeScript("$('body').removeHighlight().highlight('" + text + "')");
}

private void clearHighlights(WebEngine engine) {
    engine.executeScript("$('body').removeHighlight()");
    txtSearch.clear();
}

Help.adoc passthrough imports:

<script type="text/javascript" src="scripts/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="scripts/jquery.highlight-5.js"></script>

asciidoctor.css .highlight class:

.highlight{background-color:yellow}

1.6. Enhancement Added: Suggesting Commands for Typos

1.6.1. For the User


y or yes or k or ok or yea or yeah: Confirming a suggested command (Since v1.5)

Confirms a command suggested by Rolodex.
Format: y or yes or ok or yea or yeah

A command will be suggested by Rolodex if you make a typo in your command.

Examples:

  • s3, a typo for the command s 3 will be suggested. y or yes or k or ok or yea or yeah executes s 3 as normal and Charlotte Oliviero will be selected:

SuggestionSelect
  • rmke3 Likes to swim will be suggested as the formatted command, rmk 3 r/Likes to swim. y or yes or k or ok or yea or yeah executes rmk 3 r/Likes to swim as normal and Likes to swim will be added to Charlotte Oliviero:

SuggestionRemark
  • add Hansel Black 410 Madison Avenue NY 10029 friends neighbours ab@de.fg 98437653 will be suggested as the formatted command, add n/Hansel Black p/98437653 e/ab@de.fg a/410 Madison Avenue NY 10029 t/friends t/neighbours. y or yes or k or ok or yea or yeah executes the add command as normal and Hansel Black with appropriate fields will be added to the Rolodex:

SuggestionAdd

1.6.2. Why would we need this?

Users may find it too much of a hassle to remember many of the syntaxes required by Rolodex such as n/, r/, etc. Suggesting commands for such occasions would give the user a shallow learning curve and improve user experience significantly.

1.6.3. Implementation


Suggestions Mechanism

The suggestion mechanism is implemented at Rolodex’s parser level under the logic component. The suggestion is stored as an instance object to be evaluated only if the next command matches any of the suggestion’s affirmation commands, y, yes, k, ok, yea or yeah, otherwise, the suggestion is destroyed. Upon receiving a bogus command, the command string is evaluated by the suggestion object which determines if it can be turned into a valid suggestion to be executed.

For this section’s explanation of the suggestions mechanism, the terms commandWord, arguments or rawArgs are usually a reference to the following:

SuggestionParseLabel
  • commandWord: The first resulting word in a whitespace delimited string.

  • rawArgs: The raw argument string to be parsed for the suggestion, after excluding the commandWord.

  • arguments: Can be the same meaning or rawArgs or mean arguments in a broader context. Please adjust to the context if necessary.

The high level sequence diagram for a bogus udon is yummy request can be seen below:

SuggestionHighLevelSequenceDiagram
  1. User enters a bogus command udon is yummy.

  2. RolodexParser parses the command and separates the string udon is yummy into the commandWord, udon and arguments, is yummy.

    • udon is not a recognized command word

    • The execution is passed on to a new Suggestion instance.

  3. Suggestion determines which command abbreviation udon is closest to up to a specified threshold, which in this case, is undo.

    • Since undo does not require any arguments, the supplied arguments is yummy are ignored

    • The suggestion is noted as a suggestible exception.

  4. The suggestion is stored as an instance variable in RolodexParser.

  5. RolodexParser throws a SuggestibleParseException to be handled separately from ParseException by UI elements.

    • The CommandBox is cleared to make way for the user to easily enter any of the suggestion affirmation commands.

    • The ResultDisplay panel displays a formatted string for the suggestion of the command to be alternatively executed.

  6. At this point, the user enters an affirmation command, k for the RolodexParser to be handled.

  7. As a suggestion is present in RolodexParser, the user input k is converted into the parsed Suggestion and handled as usual.

The behaviour of the undo operation remains unchanged, as explained in the [Undo/Redo Mechanism] section. The suggestions are not limited undo commands and are applicable to all commands in the Rolodex.
Levenshtein distance is used to determine the closest command to the whitespace delimited first user input. If two command abbreviations are of equal distance to the bogus word, an arbitrary command is selected for the suggestion.

The suggestions are handled differently by different types of Command.

Commands with No Arguments

The behaviour of commands that are known to have no arguments by default, namely,

  • Clear

  • List

  • History

  • Exit

  • Help

  • Undo

  • Redo

are handled in a manner similar to undo as described above. The arguments are ignored.

Commands with a Single Index Type Argument

The commands that hold a single, indexed type argument are

  • Delete

  • Select

These commands require an argument of an index type, within the range from 1 to the total number of persons in the Rolodex. The behaviour of index parsing can be seen as such:

public static int parseFirstIndex(String value, int rolodexSize) throws NumberFormatException {
    Matcher m = Pattern.compile(INDEX_VALIDATION_REGEX).matcher(value);
    while (m.find()) {
        int firstIndex = Math.abs(Integer.parseInt(m.group()));
        if (firstIndex > 0 && firstIndex <= rolodexSize) {
            return firstIndex;
        }
    }
    throw new NumberFormatException();
}

The algorithm searches for instances of an integer in the supplied string and checks if it falls within the possible range of indices. If no more integers can be found, the method throws a NumberFormatException to be converted into an non-suggestible ParseException.

Sometimes users may include the index in the command word, without a whitespace separation. To handle such a case, we first look for the index within the arguments, as usual. If no valid indices are found, we search the command word instead.

For example, the invalid command string s3lect,

SuggestionSelectInvalid
  1. RolodexParser determines that the closest command word to s3lect is select and use parse logic for single index typed arguments.

  2. The parser would attempt to search for an index in the empty arguments and fail.

  3. The parser then attempts to search for an index in the command word and finds 3.

  4. The parser builds the arguments into the formatted command string, select 3 and prompts the user:

SuggestionSelectPrompt

Should the user choose to execute the prompted command, the person Alpha Charlie would be selected.

Commands with Directory Type Arguments

The commands that hold a single file path argument are

  • Open

  • New

Arguments for these commands are parsed by using the same filepath validation as seen in Open File and New File Mechanism, except with looser regex that does not validate from the beginning to the end of the string given.

public static final String FILEPATH_REGEX_NON_STRICT = "(.+)/([^/]+)";

The rest of the string is then formatted into a valid file path and a .rldx extension is added to the end of the string.

public static String parseFirstFilePath(String value) throws IllegalArgumentException {
    Matcher m = Pattern.compile(FILEPATH_REGEX_NON_STRICT).matcher(replaceBackslashes(value).trim());
    if (m.find() && isValidRolodexStorageFilepath(m.group())) {
        return m.group().replaceAll(ROLODEX_FILE_EXTENSION, "").trim() + ROLODEX_FILE_EXTENSION;
    }
    throw new IllegalArgumentException();
}
Find Command Suggestions

The find command uses a simpler suggestion format by simply taking all arguments, checking if it is not empty and returning it all together.

public static String parseArguments(String rawArgs) {
    // Check if null and is a non-empty string.
    requireNonNull(rawArgs);
    if (!rawArgs.trim().isEmpty()) {
        return " " + rawArgs.trim();
    }
    return null;
}
Remark/Email Command Suggestions

The Remark and Email command are in the format of

  1. a command word followed by,

  2. an index then,

  3. a memo field.

For the remark command, the memo is simply the remark to be entered into the person’s remark field, preceded by the prefix r/.
The email command’s memo field is the subject of the email to be sent, preceded by the prefix s/.

Similar to Commands with a Single Index Type Argument, if the index does not exist in the arguments, the command word is checked instead.

public static String parseArguments(String commandWord, String rawArgs) {
    // Check if index (number) exists, removes Remark prefix (if it exists) and re-adds it before returning.
    if (isParsableIndex(rawArgs, getLastRolodexSize())) {
        String indexString = Integer.toString(parseFirstIndex(rawArgs, getLastRolodexSize()));
        String remark = parseRemoveFirstIndex(rawArgs, getLastRolodexSize()).trim().replace(PREFIX_REMARK.toString(), "");
        return " " + indexString + " " + PREFIX_REMARK + remark;
    } else if (isParsableIndex(commandWord, getLastRolodexSize())) {
        String indexString = Integer.toString(parseFirstIndex(commandWord, getLastRolodexSize()));
        String remark = rawArgs.trim().replace(PREFIX_REMARK.toString(), "");
        return " " + indexString + " " + PREFIX_REMARK + remark;
    }
    return null;
}

However, instead of stopping at the index, the remainder of the arguments after the index has been removed is parsed as the memo field’s contents.

Add Command Suggestions

When adding a new person, we have to note that a Person has four compulsory fields, Name, Phone, Email and Address. Tag and Remark are optional and have to be handled accordingly.

The implementation for Add suggestions follow a decision flow model that can be seen through the following activity diagram, from AddCommandParser.parseArguments(rawArgs) execution point of view:

SuggestionAddActivityDiagram

During the handling of a suggestible command arguments for an Add command,

  1. rawArgs is set to be the initial value of the remaining arguments.

  2. Mandatory Phone is checked first. If present, extract, remove and continue. Otherwise stop execution and return as non-suggestible.

    • Phones are expected to match the phone regex,

    • having a minimum of digits, or

    • optionally prepended with a +,

    • making it easily distinguishable from other numeric formats like the index, building numbers or postal codes.

  3. Mandatory Email is checked. If present, extract, remove and continue. Otherwise stop execution and return as non-suggestible.

    • Emails are expected to have the @ character inside them and regex matching is straightforward,

    • making emails easily distinguishable from other fields.

  4. Existing Tag words are checked. If present, extract, remove and continue. Otherwise continue.

    • Words (remaining string delimited by whitespace) that are also tags in the Rolodex session are existing tag words.

  5. Tag prefixes (t/) are checked. If present, extract, remove and continue. Otherwise continue.

    • t/TAG behavior as per normal.

  6. Remark prefixes (r/) are checked. If present, extract, remove and continue. Otherwise continue.

    • r/REMARKS behavior as per normal.

  7. Mandatory Address is checked. If present, extract, remove and continue. Otherwise stop execution and return as non-suggestible.

    • Addresses commonly begin with Blk, Block or a short number

    • Addresses are expected to be entered after a name (i.e. COMMAND NAME …​ ADDRESS), making it highly possible to obtain the address by getting the substring from the first occurrence of Blk, Block or the number to the end of the remaining string.

  8. Mandatory Name is checked. If present, extract, remove and continue. Otherwise stop execution and return as non-suggestible.

    • Name is the last mandatory argument to be parsed.

    • Name must be non-empty and matches the defined name regex.

    • The unpredictable nature of name words makes it difficult to identify a person’s name.

    • As seen in Address, if Name is entered after (to the right of) Address, there would be no remaining words to be parsed. Since Name must be non-empty, if it is empty, then Name is not present.

  9. Build the arguments into a formatted command argument string.

As observed via the decision flow, the suggestion does not always suggest the correct format so it is up to the user to validate the command.

Any missing compulsory fields will result in an immediate failure of the command as specified by a person’s requirements. Optional arguments, on the other hand, may not be present and are represented as empty strings when sent to the final argument builder.

If any of the prefixes are missing, they will be added when the parsed arguments are built, with the exception of remarks and tags with prefixes.

Given a typical unformatted add command string without prefixes add Bobby Lee bobby@lee.com 583 Lexington Avenue NY 10048 friends 95849301:

AddBobbyCommand

We should receive the following suggestion Did you mean add n/Bobby Lee p/95849301 e/bobby@lee.com a/583 Lexington Avenue NY 10048 t/friends?:

AddBobbySuggestion

Using the decision flow modeled above, we can see that the argument suggestions were parsed in the following order:

  1. Bobby’s phone, 95849301 is detected as a long number and is extracted first.

  2. The email, bobby@lee.com is detected as the @ character is present and is extracted.

  3. The tag, friends is an existing tag in the currently loaded Rolodex and we can extract friends as a tag.

  4. The address, 583 Lexington Avenue NY 10048 begins with a short number and will be picked up till the end of the remaining string.

  5. The remainder of the arguments should only contain Bobby Lee and is matched as his name.

As the Add command’s decision flow requires many acyclic execution paths to its parser, its corresponding NPath complexity is known to be large.
Edit Command Suggestions

Unlike the Add command, the Edit command only requires optional (minimum 1) fields and we handle all fields as such. However, the Edit command also requires a mandatory argument of an index type, similar to Remark/Email Command Suggestions.

The implementation for Edit suggestions follow a decision flow model that can be seen through the following activity diagram, from EditCommandParser.parseArguments(commandWord, rawArgs) execution point of view:

SuggestionEditActivityDiagram

During the handling of a suggestible command arguments for an Edit command,

  1. rawArgs is set to be the initial value of the remaining arguments.

  2. Mandatory Index is checked in the remaining arguments first. If present, extract, remove and continue. Otherwise,

    • Mandatory `Index is checked in command word. If present, extract, remove and continue. Otherwise stop execution and return as non-suggestible.

    • Index extraction is similar to behavior in Commands with a Single Index Type Argument section.

  3. Optional Phone is checked first. If present, extract, remove and continue. Otherwise continue.

    • Phones are expected to match the phone regex,

    • having a minimum of digits, or

    • optionally prepended with a +,

    • making it easily distinguishable from other numeric formats like the index, building numbers or postal codes.

  4. Optional Email is checked. If present, extract, remove and continue. Otherwise continue.

    • Emails are expected to have the @ character inside them and regex matching is straightforward,

    • making emails easily distinguishable from other fields.

  5. Existing Tag words are checked. If present, extract, remove and continue. Otherwise continue.

    • Words (remaining string delimited by whitespace) that are also tags in the Rolodex session are existing tag words.

  6. Tag prefixes (t/) are checked. If present, extract, remove and continue. Otherwise continue.

    • t/TAG behavior as per normal.

  7. Remark prefixes (r/) are checked. If present, extract, remove and continue. Otherwise continue.

    • r/REMARKS behavior as per normal.

  8. Optional Address is checked. If present, extract, remove and continue. Otherwise continue.

    • Addresses commonly begin with Blk, Block or a short number

    • Addresses are expected to be entered after a name (i.e. COMMAND NAME …​ ADDRESS), making it highly possible to obtain the address by getting the substring from the first occurrence of Blk, Block or the number to the end of the remaining string.

  9. Optional Name is checked. If present, extract, remove and continue. Otherwise continue.

    • Name is the last argument to be parsed.

    • Name must be non-empty and matches the defined name regex.

    • The unpredictable nature of name words makes it difficult to identify a person’s name.

    • As seen in Address, if Name is entered after (to the right of) Address, there would be no remaining words to be parsed. Since Name must be non-empty, if it is empty, then Name is not present.

  10. If no arguments exist (i.e. Name, Phone, Email, Address, Tag, Remark are all empty strings), return as non-suggestible. Otherwise, build the arguments into a formatted command argument string.

As observed via the decision flow, the suggestion does not always suggest the correct format so it is up to the user to validate the command.

Any missing or invalid index would result in an immediate failure of the command as specified by the command specifications. Optional arguments, on the other hand, may not be present and are represented as empty strings when sent to the final argument builder. However, if completely no arguments exist (e.g. edit 1), the Edit parser would fail a well, defaulting to the original behavior.

If any of the prefixes are missing, they will be added when the parsed arguments are built, with the exception of remarks and tags with prefixes.

Given a typical unformatted edit command string with an invalid address prefix edit1 p/911A Lexington Avenue:

EditBobbyCommand

We should receive the following suggestion Did you mean edit 1 a/911A Lexington Avenue?:

EditBobbySuggestion

Using the decision flow modeled above, we can see that the argument suggestions were parsed in the following order:

  1. The index, 1 is incorrectly placed within the command word. 911 is attempted but since 911 is greater than the Rolodex’s size, no index can be found in the arguments.

  2. The index, 1 is matched in the command word and extracted.

  3. No phone is matched. 911 is detected as a short number and is skipped. The prefix p/ is removed.

  4. No email is matched.

  5. No tags are matched.

  6. No remarks are matched.

  7. The address, 911A Lexington Avenue begins with a short number and will be picked up till the end of the remaining string.

  8. The remainder of the arguments are empty when trimmed.

  9. Address is the only valid optional field present and the command is suggested as edit 1 a/911A Lexington Avenue.

As the Edit command’s decision flow requires many acyclic execution paths to its parser, its corresponding NPath complexity is known to be large.
Design Considerations
Add/Edit Field Matching

Alternative 1 (current choice): Parsing easy fields first, then parsing address before name in the Add command.
Pros: Application behaves sufficiently well and efficiently in matching field data given certain input assumptions.
Pros: Shallow learning curve for new users to learn application functions.
Cons: Extremely high NPath complexity
Cons: Deviates from the Object-Oriented nature of the codebase into a procedural/functional style.

Alternative 2: Dictionary analysis of field data.
Pros: Easier logic to understand for new developers.
Cons: Maintaining dictionary would be taxing on the development team and may get outdated as data keeps getting outdated.
Cons: Memory performance decreases drastically as memory has to be used to load dictionary into memory for field analysis.
Cons: Storage performance decreases drastically as space has to be used to store dictionary.

Alternative 3: No suggestions for add/edit
Pros: Clean codebase.
Cons: Steep learning curve for new users to learn application functions.


1.7. Enhancement Proposed: Markdown/HTML parser

To replace the content field of the note/remark command with 'rich-text', to be displayed on the browser panel.

1.8. Other contributions

  • Added continuous integration tools using Travis CI, AppVeyor, Coveralls and Codacy.

  • Added a simple navbar to the github pages for ease of navigation.

  • Managed projects and devOps.

2. Project: AutoNameGenerator

3. Other projects

You can view my full portfolio here.