LookMeUp is a brownfield software project based off AddressBook Level-3, taken under the CS2103T Software Engineering, at National University of Singapore.
AddCommandHelper
was reused with minimal changes from Snom.Fuzzy Input
was adapted from geeksforgeeks.Refer to the guide Setting up and getting started.
The Architecture Diagram given above explains the high-level design of the App.
Given below is a quick overview of main components and how they interact with each other.
Main components of the architecture
Main
(consisting of classes Main
and MainApp
) is in charge of the app launch and shut down.
The bulk of the app's work is done by the following four components:
UI
: The UI of the App.Logic
: The command executor.Model
: Holds the data of the App in memory.Storage
: Reads data from, and writes data to, the hard disk.Commons
represents a collection of classes used by multiple other components.
How the architecture components interact with each other
The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command remove 1
.
Each of the four main components (also shown in the diagram above),
interface
with the same name as the Component.{Component Name}Manager
class (which follows the corresponding API interface
mentioned in the previous point.For example, the Logic
component defines its API in the Logic.java
interface and implements its functionality using the LogicManager.java
class which follows the Logic
interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below.
The sections below give more details of each component.
The API of this component is specified in Ui.java
The diagram below represents a partial implementation of the UI components of LookMeUp
The UI consists of a MainWindow
that is made up of parts e.g.CommandBox
, ResultDisplay
, PersonListPanel
, StatusBarFooter
etc. All these, including the MainWindow
, inherit from the abstract UiPart
class which captures the commonalities between classes that represent parts of the visible GUI.
The CommandHelperWindow
has a different implementation from the rest of the UI components, which will be explained in detail at the Add By Step feature. This is to avoid cluttering the architecture diagram.
The UI
component uses the JavaFx UI framework. The layout of these UI parts are defined in matching .fxml
files that are in the src/main/resources/view
folder. For example, the layout of the MainWindow
is specified in MainWindow.fxml
The UI
component,
Logic
component.Model
data so that the UI can be updated with the modified data.Logic
component, because the UI
relies on the Logic
to execute commands.Model
component, as it displays Person
object residing in the Model
.API : Logic.java
Here's a (partial) class diagram of the Logic
component:
The sequence diagram below illustrates the interactions within the Logic
component, taking execute("remove 1")
API call as an example.
Note: The lifeline for RemoveCommandParser
should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
How the Logic
component works:
Logic
is called upon to execute a command, it is passed to an AddressBookParser
object which in turn creates a parser that matches the command (e.g., RemoveCommandParser
) and uses it to parse the command.Command
object (more precisely, an object of one of its subclasses e.g., RemoveCommand
) which is executed by the LogicManager
.Model
when it is executed (e.g. to remove a person).Model
) to achieve.CommandResult
object which is returned back from Logic
.Here are the other classes in Logic
(omitted from the class diagram above) that are used for parsing a user command:
How the parsing works:
AddressBookParser
class creates an XYZCommandParser
(XYZ
is a placeholder for the specific command name e.g., AddCommandParser
) which uses the other classes shown above to parse the user command and create a XYZCommand
object (e.g., AddCommand
) which the AddressBookParser
returns back as a Command
object.XYZCommandParser
classes (e.g., AddCommandParser
, RemoveCommandParser
, ...) inherit from the Parser
interface so that they can be treated similarly where possible e.g, during testing.How the AddCommandHelper works:
ParserUtil
to check whether the input by the user is valid. This will be explained in detail in the AddByStep Feature.API : Model.java
The Model
component,
Person
objects (which are contained in a UniquePersonList
object).Person
objects (e.g., results of a search query) as a separate filtered list which is exposed to outsiders as an unmodifiable ObservableList<Person>
that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change.UserPref
object that represents the user’s preferences. This is exposed to the outside as a ReadOnlyUserPref
objects.Model
represents data entities of the domain, they should make sense on their own without depending on other components)Note: An alternative (arguably, a more OOP) model is given below. It has a Tag
list in the AddressBook
, which Person
references. This allows AddressBook
to only require one Tag
object per unique tag, instead of each Person
needing their own Tag
objects.
API : Storage.java
The Storage
component,
AddressBookStorage
and UserPrefStorage
, which means it can be treated as either one (if only the functionality of only one is needed).Model
component (because the Storage
component's job is to save/retrieve objects that belong to the Model
)Classes used by multiple components are in the seedu.addressbook.commons
package.
This section describes some noteworthy details on how certain features are implemented.
The undo/redo mechanism is facilitated by VersionedAddressBook
. It extends AddressBook
with an undo/redo history,
stored internally as an addressBookStateList
and currentStatePointer
. Additionally, it implements the following
operations:
VersionedAddressBook#commit()
— Saves the current address book state in its history.VersionedAddressBook#undo()
— Restores the previous address book state from its history.VersionedAddressBook#redo()
— Restores a previously undone address book state from its history.These operations are exposed in the Model
interface as Model#commitAddressBook()
, Model#undoAddressBook()
and
Model#redoAddressBook()
respectively.
Given below is an example usage scenario and how the undo/redo mechanism behaves at each step.
VersionedAddressBook
will be initialized with the
initial address book state, and the currentStatePointer
pointing to that single address book state.remove 5
command to remove the 5th person in the address book followed by a yes
confirmation. The confirmation command calls Model#commitAddressBook()
, causing the modified state of the address book
after the removal execution to be saved in the addressBookStateList
, and the currentStatePointer
is
shifted to the newly inserted address book state.add n/David …
to add a new person. The add
command also calls
Model#commitAddressBook()
, causing another modified address book state to be saved into the addressBookStateList
.Note: If a command fails its execution, it will not call Model#commitAddressBook()
, so the address book state
will not be saved into the addressBookStateList
.
undo
command. The undo
command will call Model#undoAddressBook()
, which will shift the currentStatePointer
once
to the left, pointing it to the previous address book state, and restores the address book to that state.Note: If the currentStatePointer
is at index 0, pointing to the initial AddressBook state, then there are no
previous AddressBook states to restore. The undo
command uses Model#canUndoAddressBook()
to check if this is the
case. If so, it will return an error to the user rather than attempting to perform the undo.
The following sequence diagram shows how an undo operation goes through the Logic
component:
Note: The lifeline for UndoCommand
should end at the destroy marker (X) but due to a limitation of PlantUML, the
lifeline reaches the end of diagram.
Similarly, how an undo operation goes through the Model
component is shown below:
The redo
command does the opposite — it calls Model#redoAddressBook()
, which shifts the currentStatePointer
once to the right, pointing to the previously undone state, and restores the address book to that state.
Note: If the currentStatePointer
is at index addressBookStateList.size() - 1
, pointing to the latest address
book state, then there are no undone AddressBook states to restore. The redo
command uses Model#canRedoAddressBook()
to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
list
. Commands that do not modify the address book, such as
list
, will usually not call Model#commitAddressBook()
, Model#undoAddressBook()
or Model#redoAddressBook()
.
Thus, the addressBookStateList
remains unchanged.clear
, which calls Model#commitAddressBook()
. Since the currentStatePointer
is not
pointing at the end of the addressBookStateList
, all address book states after the currentStatePointer
will be
purged. Reason: It no longer makes sense to redo the add n/David …
command. This is the behavior that most modern
desktop applications follow.The following activity diagram summarizes what happens when a user executes a new command:
Aspect: How undo & redo executes:
Alternative 1 (current choice): Saves the entire address book.
Alternative 2: Individual command knows how to undo/redo by itself.
remove
, just save the person being removed).The feature to remove contacts from the address book is facilitated by RemoveCommand
and RemoveConfirmation
.
The safe-removal mechanism consists of several components:
RemoveCommand
: A class that takes in the Index
of a contact in the list, and "spotlights" this contact through
Model#getFilteredPersonList()
to then prompt the user to confirm the removal of the target person. This class
does not perform the actual removal of the contact.RemoveCommandParser
: A class that parses the user input to determine the target person to be removed. The class
parses the Index
input when users key in remove INDEX
, to proceed with the confirmation process of the actual
contact to be removed.Below is the sequence diagram outlining the execution of RemoveCommand
.
Note: The lifeline for RemoveCommandParser
should end at the destroy marker (X) but due to a limitation of PlantUML, the
lifeline reaches the end of diagram.
RemoveConfirmation
, RemoveSuccess
and RemoveAbortion
: Classes that prompt the user to confirm the removal of
the target person, performing the actual deletion of the contact (or abortion of process), then providing feedback on
the success or failure of the removal process.Below is the sequence diagram outlining the execution of RemoveSuccess
, where the user confirms the removal of a contact.
Our implementation follows Liskov's Substitution Principle closely. RemoveConfirmation
was designed to be an abstract
class to allow for extension of the 2 confirmation methods via the RemoveSuccess
and RemoveAbortion
classes. This
decision makes it easier to group similar methods and messages together for better code extendability and
maintainability when it comes to enhancing the confirmation process.
Given below is an example usage scenario and how the safe-removal mechanism behaves at each step.
Assuming existing contacts in the address book (shown in a simplified list for ease of understanding):
- Paul Walker
- Alice Cooper
- Dylan Walker
- Paul Cooper
Step 1: The user executes remove 4
command.
remove
command calls RemoveCommandParser#parseCommand()
, causing RemoveCommand#execute()
to get calledRemoveCommand
will proceed with the confirmation process of the actual contact to be removed.
RemoveCommandParser
to obtain the intended Index
to be removed.yes
or no
to confirm or abort the removal process.Are you sure you want to remove the following contact? (yes/no):
- Paul Cooper
Step 2a: The user confirms the removal of the contact by executing yes
command.
yes
command calls RemoveSuccess#execute()
to confirm the removal process.RemoveSuccess
and its parent class RemoveConfirmation
.
RemoveSuccess#execute()
checks if the yes
input is valid, calling RemoveConfirmation#isValidInput()
RemoveConfirmation#isValidInput()
returns true
if the input is valid, and false
otherwise.
remove INDEX
command, that serves as a precursor to the removal confirmation process.yes
, RemoveSuccess
will proceed with the removal process.
RemoveSuccess
will provide feedback on the success of
the removal process.Step 2b: The user aborts the removal of the contact by executing no
command.
no
command calls RemoveAbortion#execute()
to abort the removal process.RemoveAbortion
and its parent class RemoveConfirmation
.
RemoveAbortion#execute()
checks if the no
input is valid, calling RemoveConfirmation#isValidInput()
RemoveConfirmation#isValidInput()
will return true
if the input is valid, and false
otherwise.
remove INDEX
command, that serves as a precursor to the removal abortion process.no
, RemoveCommand
will abort the removal process.
CommandBox
cleared, and RemoveAbortion
will provide feedback on the abortion of the removal process.Step 2c: The user enters an invalid command e.g. abc
instead of yes
/no
after the remove 4
command.
Unknown Command
remove 1
to be prompted with the confirmation process again, or type list
to return to the default listHere is an activity diagram that summarizes the process of removing a contact from the address book:
Note: The path from the guard condition [User executes "remove 1"]
is supposed to point to Parse index
action,
but due to the constraints of PlantUML, it has been simplified to point directly to the merge node below.
Several design considerations were taken into account when implementing the safe-removal feature.
Aspect 1: Mechanism to perform the actual deletion upon confirmation
Given the key purpose of this feature is for SAFE deletion, this step is crucial to ensure that there is a safety net for users before the actual removal of the contact.
Alternative 1 (current choice): To prompt the users for confirmation via a yes
/no
, then proceed with
parsing the yes
/no
user input as independent commands in AddressBookParser
AddressBookParser
)remove INDEX
input,
currently implemented in RemoveConfirmation#isValidInput()
, leading to a more complex implementation.Alternative 2: To create functions that directly handle the confirmation process within RemoveCommand
RemoveCommand
, leading to a more monolithic class structure. This would make it harder to
maintain and extend the code in the future, as the class would be responsible for the confirmation processes
AND the actual process of removing the contact, violating the Single Responsibility Principle.Decision:
Weighing the pros and cons of Alternatives 1 and 2, we have decided to go with Alternative 1.
Addressing the cons of Alternative 1, our current implementation is such that details of previous command are retrieved
from RemoveCommandParser
within the RemoveConfirmation#isValidInput()
method. This avoids exposure of the
remove INDEX
command details, ensuring Separation of Concerns and adhering to the Single Responsibility Principle as
the RemoveConfirmation
class solely handles the confirmation process itself and checks directly related to it.
Aspect 2: Potential safe-removal enhancement by first shortlisting the contact to be removed before
proceeding with remove INDEX
, potentially reducing the amount of scrolling to find the contact to be removed.
Alternative 1: To use the same command word (i.e. remove
- remove NAME
then remove INDEX
)
to perform the shortlisting of contacts with matching names, then identifying the specific contact to be removed by its
index.
remove
for both shortlisting and confirmation processes reduces the cognitive load,
allowing the process to be more user-friendlyRemoveCommand
constructor.RemoveCommandParser
will have to handle cases where
contact names contain numbers, and users seek to shortlist contacts with the numbers in the name, to avoid invoking
remove INDEX
instead.Alternative 2 (current choice): To encourage users to use the existing find
command to shortlist the contact(s)
to be removed, then use the remove INDEX
to identify the contact from a shorter list, proceeding with safe-removal.
Decision:
Weighing the pros and cons of Alternatives 1 and 2, we have decided to go with Alternative 2 due to the clarity of
separation between the shortlisting and confirmation processes. Since this workflow is simply an enhancement to the
removal process, and given how find
is relatively intuitive to use, we believe that the maintaining the separation of
shortlisting and removal using the existing find
command would ultimately provide a more straightforward and intuitive
experience to users.
Other considerations:
RemoveCommand
constructor) ensures that the command structure is clear
and intuitive for future developers. This design decision promotes better code maintainability and extensibility,
as the shortlisting process can be easily modified without affecting the confirmation process, especially since they
are separate concerns to begin with. By adhering to the Separation of Concerns Principle, it has also ensured that
the RemoveCommand
class adheres to the Single Responsibility Principle, as it is solely responsible for the
confirmation process of the contact to be removed. The BK-Tree data structure was employed by the implementation of the fuzzy input to effectively find words that are close to the target word in terms of their Levenshtein distance. Each node in the tree-like data structure represents a word and its children represent words that are one edit distance away.
The fuzzy input implementation consists of several components:
BkTreeCommandMatcher
: The main BK-Tree data structure for sorting and efficiently search for similar elementsBkTreeNode
: Internal node structure used by the Bk-TreeFuzzyCommandParser
: A class demonstrating the usage of BK-tree for command parsingLevenshteinDistance
: An implementation of the DistanceFunction interface using the Levenshtein distance algorithm
Our implementation follows the SOLID principle closely. We have designed interfaces to promote flexibility, especially
complying with the Open-Close Principle. This design decision makes it easy to extend various CommandMatchers
or
DistanceFunctions
in the future, making it easier to incorporate alternative algorithms if need be.
Given below is an example usage scenario and how the fuzzy input mechanism behaves:
Step 1 : User misspelled listing command lust
instead of list
.
lust
command calls FuzzyCommandParser#parseCommand()
, causing BkTreeCommandMatcher#findClosestMatch()
to
get called in response.BkTree
would be already initialised with the list of commands before the call.
BkTree
calculates the distances between the commands and constructs the tree accordingly.findClosestMatch()
is called, it initiates a search within the BkTree
constructed.
lust
and commands stored in each BkTreeNode
.list
and AddressBookParser#parseCommand()
will proceed on to the list command
.BkTree
calls DistanceFunction#calculateDistance()
method.
Step 2 : User entered unsupported command peek
peek
command calls FuzzyCommandParser#parseCommand()
, causing BkTreeCommandMatcher#findClosestMatch()
to
get called in response.findClosestMatch()
does the same operation as Step 1
peek
and any commands stored in
BkTreeNode
will be greater than 1 which is greater than the specified distance metric.FuzzyCommandParser#parseCommand()
will return null
string to AddressBookParser#parseCommand()
null
is not a recognised command, ParseException
will be thrown.Common fuzzy search algorithm for approximate string matching were compared to determine the optimal algorithm for our AddressBook.
Alternative 1 (current choice) Bk-Tree with Levenshtein Distance Algorithm
Pros: Tree-like data structure
Cons: Require more memory, a concern for memory-constrained environment
Alternative 2 Hamming Distance
Alternative 3 Bitap Algorithm
Alternative 4 Brute Force Method
For our AddressBook implementation, the BK-Tree with Levenshtein Distance Algorithm
proved to be the optimal choice.
Its potential to extend code and efficiently handle misspelled or similar commands outweighs its memory usage and complexity of implementation.
This algorithm guarantees fast runtime performance and robustness in command parsing.
The sorting mechanism is facilitated by SortCommand
. It implements the following operations:
SortCommand#
: Constructor class which is instantiated and stores the necessary SortStrategy
based on user input.SortCommand#Executes
: Executes the necessary SortStrategy
and update the model.The sorting mechanism consists of several components:
SortStrategy
: An interface that requires implementations to define methods for sorting the address book and getting
the category associated with the sorting strategy.SortByTag
and SortByName
: These classes implement SortStrategy
interface to provide the specific strategies
of the AddressBook based on tags and names respectively.SortCommand
: Initiates the sorting by parsing user input to determine the sorting criteria and calls the appropriate
sorting class based on the input. After sorting, it then updates the list of persons in the model.Given below is an example usage scenario and how the sorting mechanism behaves at each step.
Step 1: The user launches the application for the first time, no contacts will be present in the AddressBook
.
When user add
contacts in the AddressBook
, contacts will be sorted based on their timestamp.
Step 2: The user executes sort name
command.
The sortCommand#
constructor will initialise with the sortByName
strategy stored as SortStrategy
.
sortCommand#execute
will pass the current model's AddressBook
to sortStrategy#sort
, where UniquePersonsList
will be obtained and sorted lexicographically by name
After sorting, the model will be updated to reflect the newly sorted contacts list, alongside a return statement to provide confirmation to the user.
Step 3: The user executes sort tag
command.
sortCommand#
constructor will initialise with the sortByTag
strategy stored as SortStrategy
.sortCommand#execute
will pass the current model's AddressBook
to sortStrategy#sort
, where UniquePersonsList
will be obtained and sorted lexicographically by tagsStep 4: The user executes sort
command.
sortCommand#
constructor will first verify the presence of condition input
before proceeding with
initialisation.ParseException
will be thrown and a statement will be displayed to provide
the correct input and conditions to be stated.SolidStrategy
interface was implemented for sorting functionality to adhere to SOLID principles, particularly the
Single Responsibility Principle, Interface Segregation Principle and Open/Close Principle.
Single Responsibility Principle
Open/Closed Principle
SortStrategy
interface without altering existing code.Interface Segregation Principle
sort
and getCategory
, thus, allowing different sorting
strategies to implement only the methods they need, rather than being forced to implement monolithic interface with
unnecessary methods.
Alternative 1 (current choice) sort
method of the SortStrategy
to take in AddressBook
as its parameter.
Alternative 2 sort
method of the SortStrategy
to take in model
as its parameter.
AddressBook
eventually, introducing unnecessary complexity.Alternative 1 is chosen for the following reasons:
AddressBook
containing
UniquePersonList
.
addbystep
loads up a separate window, which will prompt the users for the necessary input fields for an add
command.
When all the fields have been successfully entered by the user, the user can copy the formatted command to their
clipboard.
The architecture diagram given below explains the implementation for the UI for CommandHelperWindow
The functionality and purpose of the UI remains unchanged even though the implementation used for the UI is different.
The addbystep
feature is facilitated by the AddCommandHelper
and the CommandHelperWindow
class. The
CommandHelperWindow
serves as the UI for the user to interact with the AddCommandHelper
. The AddCommandHelper
is
responsible for accepting and checking whether the user's input is valid or not before prompting the user for the next
input field.
Given below is an example usage scenario and how the AddCommandHelper
class behaves at each step. Note that while each
step for accepting fields may come off as repetitive, the type of invalid inputs for each field is different. Thus, we
wish to illustrate examples of invalid inputs for each field.
Step 1: The user enters the addbystep
command, causing the CommandHelperWindow
to load up. It prompts the user for
the name of the new contact.
Step 2: The user enters the name of the new contact.
Step 3: The user enters the number of the new contact.
Step 4: The user enters the email of the new contact.
@
symbol) an error message will be shown
and the user will have to enter the email againStep 5: The user enters the address of the new contact.
cp
)From steps 2 - 5, attached below is an activity diagram of how the user interacts with the AddCommandHelper
when they
are keying in the necessary inputs. The AddCommandHelper
continuously validates the user's input to ensure that they
have entered all the necessary fields correctly.
Step 6: The user enters the cp
command.
cp
command will result in the formatted add
command
to be copied to the clipboard. Other inputs will result in the same prompt message at the end of Step 5Step 7: The successfully copied message will be displayed to the user, and the user can now close the
CommandHelperWindow
window.
CommandHelperWindow
, but those interactions are
meaningless, thus we will not go into the details of those interactions.Below is an activity diagram that summarizes the process of a user using the addbystep
feature.
Aspect: How to implement assistance functions to aid users in typing their commands.
Alternative 1 (current choice) Create a new helper class and GUI to prompt users for the necessary details.
ParserUtil
class to check the validity of the fields entered by the userCommandHelper
class can be implemented separately from the rest of the classes. This results in lower coupling
between the newly implemented CommandHelper
class and the remaining classes, resulting in easier maintenance and
integrationAlternative 2 Implement a command to display the format for users to follow.
CommandHelper
class, since prompts do not actually have any form of user
interactionAlternative 3 Implement a function to autocomplete commands for users.
CommandHelper
classThe feature to be able to add persons with duplicate names in the address book are facilitated by the use of the
DuplicateCommand
. It implements the following operations:
DuplicateCommand#
: Constructor class which is instantiated and stores the necessary toAdd
person object
based on user input.DuplicateCommand#Executes
: Executes the necessary addDuplicatePerson
method and updates the model.The sorting mechanism consists of several components:
addDuplicatePerson
: A method bound by the Person
, ModelManager
, AddressBook
classes that each contain
similar logic to support a SLAP form of implementation for the end execution point i.e. execute
in
DuplicateCommand
.DuplicateCommand
: Initiates the duplication by parsing user input to determine the identity of the person to add.
After duplicating, it then updates the list of persons in the model.Given below is an example usage scenario and how the feature mechanism behaves at each step.
Step 1: The user launches the application for the first time, no contacts will be present in the AddressBook
.
When user add
contacts in the AddressBook
, contacts will be sorted based on their timestamp.
Step 2: The user reaches a point where they encounter the need to have to add a separate contact, that has the exact
same name as another person in their AddressBook
.
Step 3: To continue, the user executes add /n... /e ...
to attempt to add this new person.
Step 4: The user then receives an error in their AddressBook
which alerts them that they already have such a person
in their AddressBook
, and they have the option of creating a duplicate of this contact.
Step 5: The user picks their choice and edits the command in their current CommandBox
, replacing add
with
duplicate
, leaving the rest of the arguments untouched.
Step 6: The user executes duplicate /n... /e...
command.
DuplicateCommand#
constructor will initialize with the toAdd
variable based on the created Person
object in DuplicateCommandParser
.DuplicateCommand#execute
will pass the toAdd
to the model#addDuplicatePerson
, where UniquePersonsList
is updated with the duplicated person.DuplicateCommandParser
interface was implemented to adhere to SOLID principles, particularly the Single Responsibility
Principle and Interface Segregation Principle.
addDuplicatePerson
and getPerson
,
thus, allowing DuplicateCommand
to implement only the methods they need, rather than being forced to
implement monolithic interface with unnecessary methods.
DuplicateCommand
constructor of the DuplicateCommand
to take in toAdd
as its parameter.
DuplicateCommand
constructor to take in all parameters of newly inserted person (name, address etc.)
DuplicateCommand
to parse the user inputs into a Person
and then execute the command.
Alternative 1 is chosen for the following reasons:
DuplicateCommandParser
method.
The feature to be able to overwrite a contact in the address book is facilitated by the use of the
OverwriteCommand
, given that the target contact's name already exists in LookMeUp.
It implements the following operations:
OverwriteCommand#
: Constructor class which is instantiated and stores the necessary toAdd
person object
based on user input.OverwriteCommand#Executes
: Executes the necessary setDuplicatePerson
method and updates the model.The sorting mechanism consists of several components:
setDuplicatePerson
: A method bound by the Person
, ModelManager
, AddressBook
classes that each contain
similar logic to support a SLAP form of implementation for the end execution point i.e. execute
in
OverwriteCommand
.OverwriteCommand
: Initiates the overwriting by parsing user input to determine the identity of the person to add.
After overwriting, it then updates the list of persons in the model.Given below is an example usage scenario and how the feature mechanism behaves at each step.
Step 1: The user launches the application for the first time, no contacts will be present in the AddressBook
.
When user add
contacts in the AddressBook
, contacts will be sorted based on their timestamp.
Step 2: The user reaches a point where they encounter the need to overwrite an existing contact, but has forgotten that they already have
contact in their AddressBook
, just with some differing details like address or email.
Step 3: To continue, the user executes add /n... /e ...
to attempt to add this seemingly new person.
Step 4: The user then receives an error in their AddressBook
which alerts them that they already have such a person
in their AddressBook
, and they have the option of overwriting the existing contact.
Step 5: The user picks their choice and edits the command in their current CommandBox
, replacing add
with overwrite INDEX
,
leaving the rest of the arguments untouched.
Step 6: The user executes overwrite INDEX /n... /e...
command.
OverwriteCommand#
constructor will initialize with the toAdd
variable based on the created Person
object in OverwriteCommandParser
, as well as the user's inputted index of person to be edited in the
AddressBook
.OverwriteCommand#execute
will pass the indexOfTarget
to the model#getPerson
, and will also pass the toAdd
to the model#setDuplicatePerson
, where UniquePersonsList
is updated with the duplicated person.OverwriteCommandParser
class was implemented to adhere to SOLID principles, particularly the Single Responsibility
Principle and Interface Segregation Principle.
setDuplicatePerson
and getPerson
,
thus, allowing OverwriteCommand
to implement only the methods they need, rather than being forced to
implement monolithic interface with unnecessary methods.
OverwriteCommand
constructor of the OverwriteCommand
to take in toAdd
as its parameter.
OverwriteCommand
constructor to take in all parameters of newly inserted person (name, address etc.)
OverwriteCommand
to parse the user inputs into a Person
and then execute the command.
Alternative 1 is chosen for the following reasons:
OverwriteCommandParser
method.
The sorting mechanism is facilitated by FilterCommand
. It implements the following operations:
FilterCommand#
: Constructor class which is instantiated and stores the necessary Predicate
based on user input.FilterCommand#Executes
: Updates the model based on the generated Predicate
.The filtering mechanism consists of several components:
Predicate
: An instance of the TagContainsKeywordsPredicate
class that stores the list of tags that a user inputs.FilterCommand
: Initiates the sorting by parsing user input to determine the filtering criteria and
then updates the list of persons in the model via model#updateFilteredPersonList
.Given below is an example usage scenario and how the filter mechanism behaves at each step.
Step 1: The user launches the application for the first time, no contacts will be present in the AddressBook
.
When user add
contacts in the AddressBook
, contacts are originally sorted based on their timestamp. Assume that
after adding contacts, the user wants to filter by contacts that are tagged as friends.
Step 2: The user executes filter friends
command.
The filterCommand#
constructor will initialise and store the argument into predicate
.
filterCommand#execute
will pass the predicate
to model#updateFilteredPersonList
, where UniquePersonsList
will be obtained and filtered by the given predicate
After filtering, the model will be updated to reflect the newly filtered contacts list, alongside a return statement to provide confirmation to the user.
TagContainsKeywordsPredicate
class was implemented for filtering functionality to adhere to SOLID principles, particularly the
Single Responsibility Principle, Interface Segregation Principle and Open/Close Principle.
test
, thus, allowing different filtering strategies to implement only the methods they need.
FilterCommand
constructor to take in predicate
as its parameter.
TagContainsKeywordsPredicate
, before passing it directly to the FilterCommand
for execution.
FilterCommand
constructor to take in user input string as its parameter.
FilterCommand
to parse the user input and then execute the command.
The copy
command feature enhances a user experience by allowing the easy transfer of a contact's personal details directly into the clipboard of the user's operating system. The class, CopyCommand
, is an inheritance of the Command
class, that facilitates the process of copying essential information like a contact's name, email, and address, among other details. It offers users the flexibility to specify and copy multiple pieces of information simultaneously. The following example demonstrates how this command operates:
A user types in copy 1 name email
into the text field of CommandBox
. Logic
is subsequently called to execute the command, where AddressBookParser
would parse the input and return a CopyCommand
(kindly refer to here for Logic design).
When AddressBookParser#parseCommand()
is called, it makes use of switch statements to match the copy
command and calls CopyCommandParser#parse()
. CopyCommandParser#parse()
is solely responsible for: (i) checking if input argument is empty; (ii) checking if index provided is non-negative; and (iii) calling ParserUtil#parseFieldsToCopy()
to verify that fields provided by the user are of acceptable fields. By definition, acceptable fields includes only name
,phone
,email
and address
.
Note: ParseException
will be thrown and an error message will be shown to user if either (i) or (iii) is violated, while IndexOutOfBoundsException
is thrown when (ii) is violated.
CopyCommandParser#parse()
returns an instantiation of CopyCommand
where its constructor takes in Index
and a List<String> fieldsToCopyList
as arguments. When the constructor of CopyCommand
is called, the constructor removes any duplicated values (if any) from fieldsToCopyList
with the use of Java Streams. Refer to code snippet below:// Constructor
public CopyCommand(Index targetIndex, List<String> fieldsToCopyList) {
requireNonNull(targetIndex);
this.targetIndex = targetIndex;
this.fieldsToCopyList = fieldsToCopyList.stream()
.distinct()
.collect(Collectors.toList());
}
CopyCommand#execute()
is called, the contact of interest is obtained with the aid of Model#getPerson
and the string representation of the information to be copied is retrieved by CopyCommand#getInfo(Person person)
that returns a StringBuilder
. The returned StringBuilder
is then converted to StringSelection
where it would be set as the content of Clipboard
. The code snippet below shows the implementation of CopyCommand#execute()
: @Override
public CommandResult execute(Model model) throws CommandException {
requireNonNull(model);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
int zeroBasedIndex = targetIndex.getZeroBased();
int addressBookSize = model.getAddressBook().getPersonList().size();
if (zeroBasedIndex < 0 || zeroBasedIndex >= addressBookSize) { // checks if Index exceeds list of contacts
throw new CommandException(MESSAGE_PERSON_NOT_FOUND);
}
Person person = model.getPerson(targetIndex.getZeroBased()); // retrieves Person of interest
StringBuilder result = getInfo(person); // retrieves a person's information
StringSelection toCopyString = new StringSelection(result.toString().trim());
clipboard.setContents(toCopyString, null); // sets result to clipboard content
return new CommandResult(MESSAGE_SUCCESS, false, false);
}
Below is a sequence diagram of the overall Copy
operation:
Several design considerations were taken into account when implementing the copy feature. Below lists a few alternatives:
Alternative 1 (current choice): User keys in command to copy a contact's information.
Alternative 2: Utilize JavaFX to allow users to select which information to copy.
From the two alternatives, alternative 1 was ultimately conceived as it does not violate our product's requirements. We believe that this design choice would benefit typists who wish to utilise input commands to retrieve contact information.
For this feature, an exit window ExitWindow
is created to seek confirmation from user to terminate LookMeUp. ExitWindow
is packaged under UI
, along with other various parts of Ui components e.g. CommandBox
, ResultDisplay
, and PersonList
etc. Similar to other Ui components, ExitWindow
inherits from UiPart
which captures the commonalities between classes that represent the different part of the entire GUI.
The Ui layout of ExitWindow
is defined under ExitWindow.fxml
. Below elucidates how ExitWindow
is used:
exit
, or other similar commands that resolves to exit
deemed by the fuzzy input algorithm.In ExitWindow.fxml
, the Yes
button is set as the default button such that the button receives a VK_ENTER press; the Yes
button will always be in focus whenever ExitWindow
is displayed. When a positive confirmation is received, ExitWindow#yesButton()
would be called to terminate LookMeUp.
Consequently, the No
button is set as the cancel button where it would receive a VK_ESC press that hides ExitWindow
. ExitWindow#NoButton()
would be called when a negative confirmation is received.
The behaviour of these implementations follow the behaviours as specified by JavaFx.
Single Responsibility Principle
ExitWindow
maintains the responsibility of displaying exit confirmation and handling a user choice, which reduces coupling between itself and other Ui components.Alternate implementation: A text field input that requires user to enter yes/no for confirmation. This design was not conceived as it requires the handling of invalid input, as is not as simple to implement as compared to the current implementation. Moreover, confirmation utilizing buttons is more intuitive for majority of users.
This Ui feature allow users to restore previously entered commands typed in the CommandBox
, regardless of the validity of the command. Similar to the CLI, users would use the Up/Down arrow keys to navigate previously typed commands in the input history.
The class that encapsulates all the history of the commands is InputHistory
which is declared as a nested class inside CommandBox
; this is because the history of commands should be the responsibility of CommandBox
class and should not be openly accessible to other classes.
InputHistory
is instantiated whenever the constructor of CommandBox
is called. As such, there is an association between InputHistory
and CommandBox
. The implementation of InputHistory
encapsulates an ArrayList<String>
and an index-pointer. Whenever a command is received by CommandBox
, the command typed will be stored inside InputHistory
(regardless of validity), as shown by the code snippet below:
@FXML
public class CommandBox extends UiPart<Region> {
///Handles the event whenever a command is entered.
@FXML
private void handleCommandEntered() {
String commandText = commandTextField.getText();
if (commandText.equals("")) {
return;
}
try {
commandExecutor.execute(commandText); //execute command in Logic
} catch (CommandException | ParseException e) {
setStyleToIndicateCommandFailure();
} finally {
inputHistory.addToInputHistory(commandText);
commandTextField.setText(""); // clears the textfield
}
}
}
CommandBox#handleArrowKey()
is called when a KeyEvent
is detected by JavaFX event listener. With reference to the code snippet below, the function checks if InputHistory
is empty. If the history is empty, it performs nothing. Else, it checks if whether the key pressed is an Up key, or a Down key. The code snippet below shows the implementation of CommandBox#handleArrowKey()
:
private void handleArrowKey(KeyEvent event) {
String keyName = event.getCode().getName();
//Performs nothing if there is no history.
if (inputHistory.inputList.isEmpty()) {
return;
}
if (keyName.equals("Up")) {
inputHistory.decrementIndex(); //Reduces pointer by 1
setTextField(); // Sets textfield according to pointer
}
if (keyName.equals("Down")) {
inputHistory.incrementIndex(); //Increment pointer by 1
setTextField(); //Sets textfield according to pointer
}
}
When CommandBox#setTextField()
is called, it requests for the command from InputHistory#getCommand()
that is pointed by the pointer, and sets the text field of CommandBox
that is returned by the method.
How the InputHistory
index-pointer works:
Whenever a new command has been entered, the command is added into the list. The index-pointer is set to the size of the ArrayList
(i.e. it is pointing towards an empty slot in the ArrayList
).
During an Up key press, the index-pointer is decremented by one (i.e. it is pointing towards an earlier command in the history).
During a Down key press, the index-pointer is incremented by one (i.e. it is point towards a later command in the history).
Below is a sequence diagram when an Up key is pressed:
Single Responsibility Principle
CommandBox
and InputHistory
are gathered together as the two classes share the responsibilities of receiving and retrieving user inputs within the text field, hence increasing the overall cohesion of Ui components.inputHistory
is set as a private variable as no other class should have access to the internal of the class, except CommandBox
itself. This allows encapsulation and information-hiding from other classes. Setter and Getter methods of InputHistory
such as decrementIndex()
, incrementIndex()
and addToInputHistory()
etc. serve as functions to retrieve and modify the value of the class.
Both InputHistory#decrementIndex()
and inputHistory#incrementIndex()
are designed with guard clauses to prevent the index pointer from reducing below zero or exceeding beyond the bounds of the ArrayList<String>
.
Alternative Design
InputHistory
consists of an ArrayList<String>
that stores all previously typed commands. An alternative solution to using an ArrayList would be LinkedList. However, LinkedList is not adopted as Java's LinkedList is implemented as Doubly-linked list which causes more memory overhead than ArrayList. Moreover, due to regular access of elements in the collection, ArrayList is a better design decision as its get
operation runs in constant time O(1), as compared to LinkedList get
O(n). Other methods such as remove
and search
etc. were not considered in the design decision as these operations are not needed for implementing InputHistory
, but may be relevant for future extensions to the class.Target user profile: NUS students who stay on campus
Value proposition:
Priorities: High (must have) - * * *
, Medium (nice to have) - * *
, Low (unlikely to have) - *
Priority | As a … | I want to … | So that I can… |
---|---|---|---|
* * * | Student in a lot of committees | Access my contacts by groups | Easily identify the people in their different committees and CCAs |
* * * | Student | Sort the contacts alphabetically | Easily navigate the address book |
(For all use cases below, the System is LookMeUp
and the Actor is the user
, unless specified otherwise)
Use case: UC1 - Add a contact
Person that can play this role: Student in a lot of committees
MSS
add
command.Extensions
1a. User typed the add
command with an invalid format or field
add
command againSteps 1a1-1a2 are repeated until the command entered is correct.
Use case resumes from step 2.
Use case: UC2 - Remove a contact
Person that can play this role: Student who need a safety net to prevent accidental contact deletion
MSS
remove
commandExtensions
1a. User typed the remove
command with an invalid format or field
remove
command againSteps 1a1-1a2 are repeated until the command entered is correct.
Use case resumes from step 2.
3a. User declines the removal of contact.
Use case: UC3 - Filter contacts by tags
Person that can play this role: Student in a lot of committees
MSS
filter
contacts commandExtensions
1a. User typed an invalid command
Steps 1a1-1a2 are repeated until the command entered is correct.
Use case resumes from step 2.
Use case: UC4 - Sort contacts by tags
Actor: User
Person that can play this role: Student in a lot of committees
MSS
sort
contacts commandExtensions
1a. User typed an invalid command
Steps 1a1-1a2 are repeated until the command entered is correct.
Use case resumes from step 2.
Use case: UC5 - Formatting an Add Command with system prompts
Person that can play this role: Student who is unfamiliar with the format of the Add command
MSS
addbystep
commandcp
)Extensions
2a. User types an invalid detail.
Steps 2a1-2a2 are repeated until the command entered is correct.
Use case resumes from step 3.
4a. User types a input that is not the copy command.
Steps 4a1-4a2 are repeated until the copy command is entered.
Use case: UC6 - Editing a command
Person that can play this role: Student who wishes to update the contact details of a contact
MSS
edit
command.Extensions
1a. User typed the edit
command with an invalid format or field
edit
command againSteps 1a1-1a2 are repeated until the command entered is correct.
Use case resumes from step 2.
Use case: UC7 - Copying a contact's details
Person that can play this role: Student who wishes to copy the contact details of a contact
MSS
copy
command.Extensions
1a. User typed the copy
command with an invalid format or field
copy
command againSteps 1a1-1a2 are repeated until the command entered is correct.
Use case resumes from step 2.
Use case: UC8 - Undoing the last command
Person that can play this role: Student who entered a wrong command and wishes to revert his previous command
MSS
undo
command.Extensions
undo
command with no previous state-changing commands
Use case: UC9 - Redoing an undo command
Person that can play this role: Student who wishes to redo the previous undo command
MSS
redo
command.Extensions
redo
command with no previous undo command.
Use case: UC10 - Clearing all the contacts in LookMeUp
Person that can play this role: Student who wants to delete all the contacts in LookMeUp
MSS
clear
command.Use case: UC11 - Exiting the application
Person that can play this role: Student who wishes to exit the application.
MSS
exit
command.Extensions
Use case: UC12 - Retrieving a person by name
Person that can play this role: Student who wants to look up a specific contact in LookMeUp
MSS
find
command.Use case: UC13 - Overwriting a contact in LookMeUp
Person that can play this role: Student who wishes to completely change the details of an existing contact.
MSS
add
command.overwrite
command with the respective input fields.Extensions
1a. User typed the overwrite
command with an invalid format or field
overwrite
command againSteps 1a1-1a2 are repeated until the command entered is correct.
Use case resumes from step 2.
11
or above installed.Given below are instructions to test the app manually.
Note: These instructions only provide a starting point for testers to work on; testers are expected to do more exploratory testing.
Initial launch
Download the jar file and copy into an empty folder
Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.
Saving window preferences
Resize the window to an optimum size. Move the window to a different location. Close the window.
Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained.
Loading up the AddByStep Window
addbystep
into LookMeUp
John
into the GUI
Edit contact based on the details provided. eg. Name, email, address etc.
Prerequisites: Existing LookMeUp contacts list must not be empty.
(Details of name, address etc. are a placeholder for the following test cases)
Test case: edit 1 n/Alex Yeoh
Expected: Contact at index 1's name has been edited to Alex Yeoh.
Test case: edit 2 n/Bernice Yu p/91725373
Expected: Contact at index 2's name and number have been edited to Bernice Yu, 91725373 respectively.
Filter contact list based on the tag(s) provided.
Test case: filter friends
Expected: Only contacts that have the tag friends
will be shown in the contact list
Test case: filter Neighbours
Expected: Only contacts that have the tag Neighbours
will be shown in the contact list
Add a person that has an identical name to a contact in your existing LookMeUp contacts.
Prerequisites: Existing LookMeUp contacts list must not be empty.
(Details of name, address etc. are a placeholder for the following test cases)
Test case: duplicate n/Alex Yeoh a/Serangoon Crescent Street e/alexyo@example.com p/91234567
Expected: Contact with above details (Name as Alex Yeoh, Phone as 91234567...) is added
Test case: duplicate n/Bernice Yu a/Serangoon Crescent Street e/berniceyu@example.com p/91234568
Expected: Contact with above details (Name as Bernice Yu, Phone as 91234568...) is added
Test case: duplicate n
Expected: No contact is added. Error details are shown in the status message.
Overwrites a person that has an identical name to a contact in your existing LookMeUp contacts.
Prerequisites: Existing LookMeUp contacts list must not be empty.
(Details of name, address etc. are a placeholder for the following test cases)
Test case: overwrite 1 n/Alex Yeoh a/Serangoon Crescent Street e/alexyo@example.com p/91234567
Expected: Contact at index 1, and with above details (Name as Alex Yeoh, Phone as 91234567...) is overwritten
Test case: overwrite 2 n/Bernice Yu a/Serangoon Crescent Street e/berniceyu@example.com p/91234568
Expected: Contact at index 2, and with above details (Name as Bernice Yu, Phone as 91234568...) is overwritten
Test case: overwrite 1
Expected: No contact is overwritten. Error details are shown in the status message.
Removing a person while all persons are being shown
Prerequisites: List all persons using the list
command. Multiple persons in the list.
Test case: With a list of at least 2 contacts, enter remove 2
Expected: Second contact is spotlighted from the list. Prompt to confirm removal with yes
/no
.
yes
, the contact will be removed. A success message and details of the removed contact will be shown in the status message. Timestamp in the status bar is updated.no
, the contact will not be removed. Status message will show the removal is aborted. Status bar remains the same.Test case: remove 0
Expected: No person is removed. Error details shown in the status message. Status bar remains the same.
Other incorrect remove commands to try: remove
, remove x
, ...
(where x is larger than the list size)
Expected: Respective error messages. Similar to previous.
Handling a command with a single misspelled letter.
lort name
sort name
command will still be executed and contact list will be sorted alphabetically based on person's namelst
list
command will still be executed and all the contacts will be listed in the contact list.fwlter TAG
, adbystep
, ...
Sort contact list based on the keywords input.
Test case: sort name
Expected: Contact list will be sorted lexicographically based on person's name.
Test case: sort tag
Expected: Contact list will be sorted lexicographically based on person's tags.
Retrieve a contact's information into system clipboard.
Given this example:
Below shows a list of possible commands:
Sample Commands | Details | Results |
---|---|---|
copy -1 name | Copies the name of contact indexed -1 | N.A. Error will be shown. |
copy 4 tag | Copies the tag of contact indexed 4 | N.A. Tag is not a valid field. |
copy 4 name | Extra spaces between copy and index | Taylor Sheesh |
copy 4 name | Extra spaces between index and name | N.A Error prompt fields not recognised. |
For more sample test cases, kindly refer to the UG.
Enter add n/Jia wei p/97743772 e/jw@gmail.com a/Block E 02-22 t/friend
in the command box.
Expected output: A new contact named "Jia wei" will be added to your list, and will be found at the last index.
Enter undo
in the command box.
Expected output: The contact list will revert back to its state before the contact was added in Step 1.
Enter redo
in the command box.
Expected output: The contact list will revert back to the state after the contact was added as it is in Step 2.
Within the same application launch, you may try to perform n consecutive state-changing commands, then
directly followed by undo
, and expect to be able to run undo
n consecutive times as well.
Similarly, with x consecutive undo
commands, you should be able to run redo
consecutively x times as well.
Note: Examples of commands that are NOT state-changing include: filter
, list
and hence if you try to undo
,
there will be an error message that there is no command to undo.
Do also note that the redo
command must be immediately preceded with undo
, failing which (false example:
add n/...
, undo
, remove 1
, then redo
) there will be an error message that there is no command to redo.
Dealing with missing/corrupted data files
Our team consists of 5 members.
Add an exit
command to AddCommandHelper
to enhance AddByStep process
AddCommandHelper
has to be closed manually, which is not optimised for fast typists.AddCommandHelper
such that you can close the window simply by typing the
exit
commandVary Distance Metric for Fuzzy Input
addbystep
can be accurately Enhance Invalid Input Error-Handling for Safe-Removal Confirmation Step
yes
or no
input, if they enter any other
(invalid) input e.g. abc
, they will be prompted with an Unknown Command
error message, which is too general.Unknown Command
prompt, though the GUI still shows the spotlighted contact for removal, the user is
unable to directly type yes
/no
to proceed with the confirmation as the system does not recognise that the user is
still in the process of removing the contact, which brings slight inconvenience though there is a simple step to
get around it by typing remove 1
then yes
/no
to proceed with the confirmation.Unknown Command
error message to be more specific, such as Invalid Input, please enter 'yes' if you wish to proceed with the removal, and 'no' if you wish to abort the removal process
.RemoveConfirmation#isValidInput
to check for whether the last VALID input is a RemoveCommand
instead of simply checking the last input (which currently makes the system prone to keeping track of the
invalid input and assuming the user is no longer doing a removal). This would remove the inconvenience of having
to type remove 1
.Improve Name Validation for Name Field
Joseph King Jr.
,
Shaquille O'Neal
, Mary-Anne Tan
or Ravichandram S/O Ramesh
are considered invalid.'
, -
, /
, and .
to be recognised