By: AY1920S1-CS2103T-F13-1
Since: Sep 2019
Licence: MIT
LiBerry is a desktop app for librarians to quickly manage their community libraries! LiBerry is optimized for librarians who prefer to work with a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). You can type quickly and serve your long line of borrowers in a short amount of time. LiBerry can manage all your books and borrowers efficiently and meticulously.
This developer guide is targeted towards potential developers of the project and it aims to explain:
-
The design of the software architecture of the system using a top-down approach
-
The implementation and behaviour of the main features of the system.
To set up LiBerry on your system, please refer to the guide here.
In this section, we will explain the design and behaviour of the top-level components in the system, which are the following:
-
Architecture overview
-
User Interface (UI) Component
-
Logic Component
-
Model Component
-
Storage Component
This sub-section shows the relationship between the major components at the highest level, illustrated by the following diagram.
The Architecture Diagram given above explains the high-level design of the App. Given below is a quick overview of each component.
💡
|
The .puml files used to create diagrams in this document can be found in the diagrams folder.
Refer to the Using PlantUML guide to learn how to create and edit diagrams.
|
-
At app launch: Initializes the components in the correct sequence, and connects them up with each other.
-
At shut down: Shuts down the components and invokes cleanup method where necessary.
Commons
represents a collection of classes used by multiple other components.
The following class plays an important role at the architecture level:
-
LogsCenter
: Used by many classes to write log messages to the App’s log file.
The rest of the system consists of four components.
Each of the four components
-
Defines its API in an
interface
with the same name as the Component. -
Exposes its functionality using a
{Component Name}Manager
class.
For example, the Logic
component (see the class diagram given below) defines it’s API in the Logic.java
interface and exposes its functionality using the LogicManager.java
class.
The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command add t/Animal Farm a/George
.
In the diagram above, we can see how the components integrate together to execute a single command.
The sections below give more details about each component, starting of with the UI component.
This sub-section shows the structure of the User Interface (UI) and the relationship between each component in the UI.
The following diagram aims to illustrate how each UI sub-component is linked to one another.
In the figure above, we can see the association between the different UI sub-components, as well as the classes that interact with the external Logic
and Model
components.
The UI consists of a MainWindow
that is made up these main parts:
-
CommandBox
-
ResultDisplay
-
BookListPanel
-
Other smaller components
All these, including the MainWindow
, inherit from the abstract UiPart
class.
API : Ui.java
The UI
component uses 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,
-
Executes user commands using the
Logic
component. -
Listens for changes to
Model
data so that the UI can be updated with the modified data.
Given below is the Sequence Diagram for interactions within
the UI component when the user enters an add command.
The exact command entered is add t/Animal Farm a/George
.
In the figure above, we can see how the UI components invoke the execute
method of the Logic
class in order to obtain and subsequently display the result of the execution.
The following activity diagram summarizes what happens to the UI
component
when a user executes a new command:
The activity diagram above aims to illustrate how UI
only updates the BookListPanel
when the catalog is being updated by a command.
We will now move on to give more details about the Logic
component.
In this sub-section, we will explain the internal workings of the Logic
component, which handles the execution of the different commands.
The following class diagram aims to show how the 'Command Design Pattern' is used to achieve a high-level form of encapsulation of the Command
object.
In the diagram above, we can see that the LogicManager
executes the Command
class without knowledge of what each command does. This is achieve through polymorphism where all possible commands extend from the Command
class.
API :
Logic.java
-
Logic
uses theCatalogParser
class to parse the user command. -
This results in a
Command
object which is executed by theLogicManager
. -
The command execution can affect the
Model
(e.g. adding a book). -
The result of the command execution is encapsulated as a
CommandResult
object which is passed back to theUi
. -
In addition, the
CommandResult
object can also instruct theUi
to perform certain actions, such as displaying help to the user.
Given below is the Sequence Diagram for interactions within the Logic
component for the execute("add t/Animal Farm a/George")
API call.
In the diagram above, we can see that the Logic
component’s execute
is invoked by the UI
component from before. A series of method calls would invoke the addBook
method of the Model
, moving the chain of calls further downstream.
ℹ️
|
The lifeline for AddCommandParser should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
|
In short, the Logic
component interprets the different commands and execute them accordingly. Most of these commands will have to interact with the Model
component, which we will explore in the next sub-section.
The Model
component is mainly composed of the Book
, Borrower
and Loan
classes and shows how they are related to one another.
The figure below shows the relationship between smaller components. These smaller components are modelled after real world objects.
The figure illustrates the composition of the Model
component. The Model
,
-
stores a
UserPref
object that represents the user’s preferences. -
stores the Catalog data.
-
stores the Loan Records.
-
stores the Borrower Records.
-
references a borrower that is being served if the model is in serve mode.
-
references a list of filtered books which depends on the state of the model.
-
exposes an unmodifiable
ObservableList<Book>
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.
API : Model.java
When there are changes in the Model
component, the system will update its in-memory via the Storage
component, which will be explained in-depth in the next section.
The Storage
component is responsible for updating the memory of the system (in JSON
format) whenever there are changes.
The figure below aims to show the different records storage that are implemented in LiBerry.
In the figure above, we can see that we are maintaining 4 different storages. These storages aim to keep the memory of:
-
UserPrefs
-
Catalog
-
BorrowerRecords
-
LoanRecords
API : Storage.java
The Storage
component,
-
can save
UserPref
objects inJSON
format and read it back. -
can save LiBerry data in
JSON
format and read it back.
There are certain classes (eg. Utility classes) that are used by different components. In the following section, we will explain how we allow all components to access these classes.
Classes used by multiple components are in the seedu.addressbook.commons
package.
These classes include (to list a few):
-
User Settings
-
Exceptions
-
Utility classes like
DateUtil
,FineUtil
andJsonUtil
We will now move on to the next section, which aims to explain the implementation of some of our main features.
This section describes some noteworthy details on how certain features are implemented.
This feature allows a user to add a new book to the LiBerry system.
The add book function is facilitated by Catalog
.
The Catalog
stores a list of books, representing the books in the library.
Additionally, it implements the following operation:
-
Catalog#addBook(book)
— Add a new book to the list of books in the catalog.
Given below is an activity diagram of a book being added to the catalog.
Given below is a class diagram of a book.
Notice how the book can hold either 1 or 0 loans, depending on whether it is currently loaned out or not.
Given below is the object diagram of a newly added book.
We can see that the book holds a Optional<Loan>
in order to have either 0 or 1 Loan
objects. This makes it consistent with the class diagram of Book
above.
-
Alternative 1 : Store them only in a ObservableList as per the original AddressBook implementation.
-
Pros: Will be easy to implement.
-
Cons: May have performance issues in terms of efficiency in retrieving books.
-
-
Alternative 2 (current choice): Store them in a HashMap.
-
Pros: Will be easier (and more readable in code) to retrieve books by serial number.
-
Cons: Will incur additional memory to maintain the HashMap.
-
Since we allow librarians to provide a valid serial number to new books if they wish so, we cannot generate the serial number using the number of books or the largest serial number.
Eg: The system now has "B00009" and "B00010".
If we generate based on number of books, we get the serial number "B00003", wasting the serial numbers "B00001" and "B00002".
If we generate based on the largest serial number, we get the serial number "B00011", wasting all the unused serial numbers before it.
We need to come up with a solution to give us "B00001" in the given example.
-
Alternative 1 (current choice): Use a TreeMap to store current serial numbers such that we can efficiently determine the next serial number in running order.
-
Pros: Will be efficient in generating the next valid serial number.
-
Cons: Will incur additional memory to maintain the TreeMap.
-
-
Alternative 2: Use brute force to start iterating from "B00001" to obtain the first unused serial number.
-
Pros: Will be easy to implement.
-
Cons: Will be inefficient once the number of books grow.
-
The undo/redo mechanism is facilitated by CommandHistory
.
It contains a undo/redo command history, stored internally as an commandHistoryList
and currentCommandPointer
.
Additionally, it implements the following operations:
-
CommandHistory#commit()
— Saves the current reversible command in its command history. -
CommandHistory#undo()
— Undoes the most recent reversible command. -
CommandHistory#redo()
— Redoes the most recent previously undone command.
These operations are exposed in the Model
interface as Model#commitCommand()
, Model#undoCommand()
and Model#redoCommand()
respectively.
The undo/redo mechanism only works for commands that implements the ReversibleCommand
interface.
The ReversibleCommand
interface specifies that the commands these two operations:
-
ReversibleCommand#getUndoCommand()
— Returns a command that undo theReversibleCommand
. -
ReversibleCommand#getRedoCommand()
— Returns a command that redo theReversibleCommand
.
Given below is an example usage scenario and how the undo/redo mechanism behaves at each step.
Step 1. The user launches the application for the first time.
The CommandHistory
will be initialized with an empty commandHistoryList
.
Step 2. The user executes delete 5
command to delete the 5th
book in the catalog. The delete
command calls Model#commitCommand()
,
causing the delete 5
command to be saved in the commandHistoryList
,
and the currentCommandPointer
is pointed to the newly inserted command.
Step 3. The user executes add t/Animal Farm …
to add a new book.
The add
command also calls Model#commitCommand()
, causing the add
command to be saved into the catalogHistoryList
.
ℹ️
|
If a command fails its execution, it will not call Model#commitCommand() ,
so the command will not be saved into the commandHistoryList .
|
Step 4. The user now decides that adding the book was a mistake,
and decides to undo that action by executing the UndoCommand
.
During the execution of the UndoCommand
, Model#undoCommand()
will be called. This would call CommandHistory#undo()
, which
will retrieve the most recent ReversibleCommand
that was executed, which is
the add
command. ReversibleCommand#getUndoCommand()
would then be called
and the Command
returned would be executed, undoing the add command.
This will then shift the currentCommandPointer
once to the left, pointing it
to the previous ReversibleCommand
in the commandListHistory
.
ℹ️
|
If the currentCommandPointer is at index -1, pointing to no command,
then there are no previous command to undo. The undo command uses
Model#canUndoCommand() 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 the undo operation works:
ℹ️
|
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.
|
The redo
command does the opposite — it calls Model#redoCommand()
,
which shifts the currentCommandPointer
once to the right, pointing to
the previously undone Command, and executes the redo command from
ReversibleCommand#getRedoCommand()
.
ℹ️
|
If the currentCommandPointer is at index catalogHistoryList.size() - 1 ,
pointing to the latest command, then there are no undone command to redo.
The redo command uses Model#canRedoCommand() to check if this is the case.
If so, it will return an error to the user rather than attempting to perform the redo.
|
Step 5. The user then decides to execute the command help
.
Commands that do not modify the model, such as help
,
will usually not call Model#commitCommand()
,Model#undoCommand()
or
Model#redoCommand()
. Thus, the commandHistoryList
remains unchanged.
Step 6. The user executes clear
, which calls Model#commitCommand()
.
Since the currentCommandPointer
is not pointing at the end of the commandHistoryList
,
all commands after the currentCommandPointer
will be purged.
We designed it this way because it no longer makes sense to redo the
add t/Animal Farm …
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:
-
Alternative 1 (current choice): Individual command knows how to undo/redo by itself.
-
Pros: Will use less memory (e.g. for
delete
, just save the book being deleted). -
Cons: We must ensure that the implementation of each individual command are correct.
-
-
Alternative 2: Saves the entire catalog.
-
Pros: Easy to implement.
-
Cons: May have performance issues in terms of memory usage.
-
Considering our target audience, community libraries, which may be poor. They might be not able to afford a large amount of data storage. As a library may contain many books, borrowers and loans, storing a state of application for each command can be memory intensive. Hence, we chose to implement Alternative 1 so as to reduce the amount of memory usage.
-
Alternative 1 (current choice): Use a list to store the commands for undo and redo.
-
Pros: Only need to maintain one data structure.
-
Cons: Harder for new developers to understand the mechanism for undo and redo.
-
-
Alternative 2: Use two stacks to store a list of undoable and redoable commands.
-
Pros: Easy for future developers to understand as there are two separate stacks to keep track of the command to undo and redo.
-
Cons: Additional time required to add and pop from the stack.
-
We chose alternative 1 as it is easier to maintain a single data structure.
The printing of loan slip feature is facilitated by LoanSlipUtil
.
Essentially, it implements the following operations:
-
LoanSlipUtil#mountLoan()
— Mounts a loan slip in preparation for generation of loan slip in pdf form. -
LoanSlipUtil#unmountLoan()
— Unmounts a loan slip ,usually after generating a pdf version of it. -
LoanSlipUtil#createLoanSlipInDirectory()
— Creates a pdf version of the mounted loan slip in the loan_slips folder.
Given below is the sequence diagram of the generation of loan slip during the loan of a book.
The following describes the sequence of events displayed in the figure above.
1) The LoanCommand
is executed
2) The LoanCommand
retrieves the Book
and the Borrower
3) The LoanCommand
creates a new Loan
4) The LoanCommand
mounts the new loan in LoanSlipUtil
5) The Storage
component creates and save a new LoanSlipDocument
in a saved folder
6) The Logic
component opens the newly generated LoanSlipDocument
for the librarian to print it immediately
7) The Logic
component unmounts the LoanSlipDocument
at the end of the process
-
Alternative 1 : Use the
LoanSlipDocument
constructor directly.-
Pros: Will be straightforward to implement.
-
Cons: The
Logic
component and theLoanCommand
object needs to have knowledge on all individual methods ofLoanSlipDocument
to be able to create a loan slip.
-
-
Alternative 2 (current choice): Create a Facade class
LoanSlipUtil
to facilitate creation ofLoanSlipDocument
.-
Pros: The
Logic
component and theLoanCommand
object can now use the full functionality ofLoanSlipDocument
via the static classLoanSlipUtil
without knowing the internal implementation ofLoanSlipDocument
. -
Cons: There is more code to be written and we must consider how to save state within a static class such that it can be continually reused.
-
We have decided to go with Alternative 2. The Facade class provides the system with a simplified view of generating a loan slip, making it easier to use. It also decouples the code, making it easier to modify in the future.
-
Alternative 1 (current choice): Mount a loan in
LoanSlipUtil
for each book.-
Pros: Will be easy to extend in the future as we can just mount multiple loans using
LoanSlipUtil
before generating all loans in a single loan slip. -
Cons: Will require more code when mounting loans in the Facade class.
-
-
Alternative 2: Re-create
LoanSlipDocument
whenever a new loan comes in.-
Pros: Will only need to make adjustments to
Logic
component to contain anOptional<LoanSlipDocument>
field and update accordingly whenever a newLoan
comes in. -
Cons: Violates Single Responsibility Principle as the Logic class will now have to change if we change the implementation of
LoanSlipDocument
.
-
We are using java.util.logging
package for logging. The LogsCenter
class is used to manage the logging levels and logging destinations.
-
The logging level can be controlled using the
logLevel
setting in the configuration file (See Section 4.5, “Configuration”) -
The
Logger
for a class can be obtained usingLogsCenter.getLogger(Class)
which will log messages according to the specified logging level -
Currently log messages are output through:
Console
and to a.log
file.
Logging Levels
-
SEVERE
: Critical problem detected which may possibly cause the termination of the application -
WARNING
: Can continue, but with caution -
INFO
: Information showing the noteworthy actions by the App -
FINE
: Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size
Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: config.json
).
The functionalities and commands associated with the book loaning feature are:
-
loan sn/BOOK_SN
-
Loans out a book based on its serial number.
-
-
return INDEX [-all]
-
Returns a book based on the index of that book shown on the GUI.
-
-
renew INDEX [-all]
-
Renews a book based on the index of that book shown on the GUI.
-
-
reserve sn/BOOK_SN
orreserve INDEX
-
Reserves a book based on its serial number or index of that book shown on the GUI.
-
-
pay AMOUNT
-
Pay a fine amount for overdue books.
-
The rationale for the different types of arguments for the loan, return, renew and reserve may not be apparent at first, but it is actually very simple.
-
When borrowers come to the librarian (our user) to borrow a book, the book itself would have a serial number. Thus, the user just types in the serial number of the book to be loaned out, instead of using the find command to locate that book in LiBerry to use its index.
-
Whereas, when a borrower comes to return a book, the librarian only sees a limited list of book that was loaned out by the borrower on the GUI. Hence, the librarian need not type the longer serial number to return that book, and instead, types its index shown in the GUI list. Likewise, the same idea is applied to renewing books
-
When reserving books for borrowers, it is possible that the book is found through the GUI, and thus, index is used. Or, the borrower brings the physical book forward to reserve it as he/she does not want to borrow it now.
This feature is mainly facilitated by the Loan
association class between a Book
and a Borrower
. The object diagram
just after a book is loaned out can be seen below.
In this instance, the Borrower
with BorrowerId
K0789 currently has a Book
with SerialNumber
B00456 loaned out.
The Loan
associated to this loan, with LoanId
L000123, is stored in the LoanRecords
class of the model component.
Both the Book
and Borrower
objects also have access to this Loan
object.
In each Loan
object, only the BorrowerId
of the Borrower
and SerialNumber
of the Book
is stored to reduce
circular dependency.
The LoanRecords
class stores all the Loan
objects tracked by LiBerry in a HashMap, where the key is its LoanId
.
Immutability of each object is supported to ensure correctness of undo and redo functionality.
The following activity diagram summarizes what happens when a user enters a loan command:
ℹ️
|
The else branch of each branch node should have a guard condition [else] but due to a limitation of PlantUML,
they are not shown.
|
When a book is successfully loaned out by a borrower, a new Loan
object is created. The LoanId
is automatically generated according
to the number of loans in the LoanRecords
object in the model. The startDate
is also automatically set to today’s date.
The endDate
is automatically set according to the loan period set in the user settings. This Loan
object is added to
LoanRecords
through the call to Model#addLoan(loan)
.
The new Borrower
instance is created by copying the details of the borrower from the original object, and also with this Loan
object being added into its currentLoanList
. The new borrower object then replaces the old borrower object in the
BorrowerRecords
object in the model. These two steps are done through the method call to Model#servingBorrowerNewLoan(loan)
.
The new Book
instance is also created by copying the details of the original book object, and likewise, with this Loan
object added into it.
Similarly, the new book object replaces the old book object in the Catalog
object in the model through the call to
Model#setBook(bookToBeLoaned, loanedOutBook)
. These were done to support immutability of the objects.
The following activity diagram summarizes what happens when a user enters a return command:
ℹ️
|
The else branch of each branch node should have a guard condition [else] but due to a limitation of PlantUML,
they are not shown.
|
When a loaned out book is successfully returned by a borrower, the associated Loan
object is moved from the borrower’s
currentLoanList
to returnedLoanList
. Inside the book object, this Loan
object is also removed. Inside this loan
object, the returnDate
is added according to today’s date. The remainingFineAmount
of this loan object is also
calculated based on the fine amount set in the user settings.
Similarly, the creation of new objects for replacement are also done to support immutability. They are supported by the
methods Model#setBook(bookToBeReturned, returnedBook)
and Model#servingBorrowerReturnLoan(returningLoan)
.
When a book is successfully renewed by a borrower, the renewCount
of the Loan
object is incremented by 1
and its dueDate
is also increased by the renew period set in the user settings.
Similarly, the creation of new objects for replacement are also done to support immutability.
When a book is successfully reserved by a borrower, the Book
object is marked as reserved and is added to the borrower’s
reservedBookList
.
Similarly, the creation of new objects for replacement are also done to support immutability.
Inside the model, for each current loan (loans that are not returned yet), the Book
, the Borrower
and the LoanRecords
point to the same the same Loan
object. LiBerry’s storage system is such that Catalog
stores the books,
BorrowerRecords
stores the borrowers and LoanRecords
stores the loans. Thus, a decision was made to decide how these
loans are serialized and stored in the user’s file system.
-
Alternative 1: Save the whole
Loan
object in each book in thecatalog.json
and save the the whole of everyLoan
object in each borrower inborrowerrecords.json
. TheLoan
object is also duplicated inloanrecords.json
.-
Pros: Easy to implement. No need to read storage files in a specific order.
-
Cons: Storage memory size issues. Same information is duplicated and stored in all 3 storage files.
-
-
Alternative 2 (selected choice): Save only the
LoanId
of eachLoan
object in each book in thecatalog.json
and save a list ofLoanId
in each borrower inborrowerrecords.json
. The wholeLoan
object is only saved inloanrecords.json
. When reading the storage files at the start of the application,loanrecords.json
need to be read in first, before the borrowers and books can be read in as they would get the loan objects from theLoanRecords
based on theirLoanId
s.-
Pros: Uses less memory as only
LoanId
is stored for the books and borrowers, instead of the whole serialized loan objects. Also,LoanRecords
thus serve as a single source of truth. -
Cons: Must ensure that the reading of stored files are in the correct order, and also correct
Loan
objects are referenced after reading inborrowerrecords.json
andcatalog.json
. Method used to retrieve aLoan
object from itsLoanId
must also be fast enough as there can be hundreds of thousands of loans.
-
-
Alternative 1: Use a list data structure, such as an
ArrayList
to store the loans in the model component.-
Pros: Easy to implement. Easy to obtain insertion order of the loans and sort through the list.
-
Cons: Slow to search for a
Loan
based on itsLoanId
, i.e., O(n) time, as the list must be traversed to find the correct associatedLoan
object. The additional time taken adds up when reading the storage files during the starting up of the application. Thus, it can make the application feel laggy and unresponsive at the start.
-
-
Alternative 2 (selected choice): Use a
HashMap
to store the loans, where the key is itsLoanId
.-
Pros: Fast to retrieve a
Loan
object based on itsLoanId
, i.e., O(1) time. -
Cons: Insertion order is not preserved. Have to traverse through all the loan objects in the HashMap to check their
startDate
in order to obtain their insertion order.
-
The command for finding a book in the catalog is as follows:
find [NUMBER] { [t/TITLE] [a/AUTHOR] [g/GENRE]… [sn/BOOK_SN]] [-overdue] [-loaned] [-available] }
ModelManager contains a FilteredList
of Books
(filteredBooks
), which is used to display books on the LiBerry GUI. Book finding works by
starting converting the command string in to a BookPredicate
object, then updating filteredBooks
with that predicate.
The parsing of the command string to create the required BookPredicate
object is done with the help of the ArgumentTokenizer
object.
ArgumentTokenizer
tokenizes the command string to generate an
ArgumentMultimap
, which is internally a HashMap of predicate values paired to prefix keys. The FindCommandParser
then extracts all the values from the ArgumentMultimap
prefix by prefix and building the predicate through functions
such as setTitle()
, setGenres()
setLoanStatus
etc.
The diagram below shows a simplifed execution sequence of a 'find t/Animal Farm a/George' command string.
The BookPredicate
class stores in its fields the specific values to match. Default values are mostly null, which will indicate that
there is no need to filter for that field. Below is an example.
The figure above shows what happens when we are trying to filter for books with title 'harry' and 'Potter' that are loaned out, showing up to 5 books only. Notice that the rest of the fields in the object are null.
In order for LiBerry to display only books that are loaned, available or overdue, flags are used. All flags have
the prefix -
, and the ArgumentTokenizer
is able to detect this. However, a user can technically enter more than 1
of such loan status flags eg. -loaned -available
. This is not meaningful, as there can be multiple interpretations of
this statement. The user could be looking for both types of books (which will show every book), or books that are both
loaned and available (which will show none). To prevent such meaningless confusion, there is a need for only 1 such
flag to be accepted in the BookPredicate
.
-
Alternative 1: Hard code a priority for loan status flags and accept the highest one
-
Pros: Easy to implement
-
Cons: Does not make it clear for the user why an unintended display is shown
-
-
Alternative 2 (Currently Used): Raise an exception whenever there are more than 1 loan status flags
-
Pros: Helps user clarify misconception of using more than 1 loan status flag
-
Cons: Slightly more complicated code where the output
ArgumentMultimap
-
As users generally do not want to be flooded with information when using the find command, a display limit [NUMBER]
is used. Users
can ask for a limited number of books to display. However, the FilteredList
JavaFx class that is used to implement the list of
filtered books does not have an API that sets a hard limit on the number of books to show. A work-around has to be made.
-
Alternative 1: Create an new class that extends the JavaFx
FilteredList
class-
Pros: Does not require a change in other parts of the code
-
Cons: Hard to implement. Need to know the ins and outs of FilteredList
-
-
Alternative 2 (Currently Used): Create a counter variable in
BookPredicate
that decrements after every passed test-
Pros: Easy to implement.
-
Cons: Not the most cleanest way of implementation
-
The register
borrower feature is facilitated by BorrowerRecords
. The BorrowerRecords stores a list of borrowers,
representing the borrowers registered into the library system. The command to register a borrower into the library
system is as followed:
register n/NAME p/PHONE_NUMBER e/EMAIL
Given below is an activity diagram of a borrower being registered into the Borrower Records of the library.
Given below is a class diagram of a book.
Given below is the object diagram of a newly registered borrower.
Every time a new borrower is being registered, the system will automatically generate a borrower ID for the borrower which the borrower will have to use every time the borrower borrows books from the library. Initially, what we proposed is that, every time a new borrower is being registered into the system, we find the size of the list of borrowers, we add 1 and set it as the borrower ID of the new borrower.
Eg: There are 100 borrowers in the system. The new borrower’s ID will be "K0101".
However, we decided to implement a new function, which is to allow borrowers to be removed from the library system. Therefore, this method does not work anymore. So we decided to change to generate the new ID based on the first-found available ID.
Refer to the guide here.
Refer to the guide here.
Refer to the guide here.
Target user profile:
-
a librarian in a small town library that has to serve many library users (borrowers) quickly
-
has a need to manage a significant number of books and borrowers
-
prefer desktop apps over other types
-
can type fast
-
prefers typing over mouse input
-
is reasonably comfortable using CLI apps
Value proposition: Many people visit the neighborhood library to borrow books and also donate their books. There is always a long queue in this small library and the librarian would have to type quickly to handle the long queue. LiBerry can manage a library system faster than a typical mouse/GUI driven app.
Priorities: High (must have) - * * *
, Medium (nice to have) - * *
, Low (unlikely to have) - *
Priority | As a … | I want to … | So that I can… |
---|---|---|---|
|
librarian |
add a book brought/donated by people to the library |
maintain a record of all the books in the library |
|
librarian |
delete books that are no longer available |
maintain a record of all the books in the library |
|
helpful librarian |
search for certain book by the title/author/genre |
help borrowers check if it is available |
|
forgetful librarian |
mark a book as loaned |
tell borrowers that the book is loaned out and unavailable for borrowing |
|
forgetful librarian |
mark a book as available |
let borrowers know that the book will now be available for borrowing |
|
librarian |
generate a list of overdue books and their borrowers |
know which borrower has overdue books and which books are overdue |
|
librarian |
generate a list of currently loaned / available books |
do inventory checks |
|
meticulous librarian |
record the movement of books in and out |
keep track of available books here |
|
helpful librarian |
register a new borrower in the system |
help new borrowers start borrowing books |
|
librarian |
search for certain book by the author |
recommend other books of the same author |
|
librarian |
search for certain book by its genre |
recommend other books of the same genre |
|
meticulous librarian |
different physical books to have different serial numbers |
distinguish between books of the same title |
|
librarian |
set the default loan period, renew period and fine amount |
customize the app to suit my library’s policies |
|
librarian |
extend a book’s loan |
help borrowers to borrow the book for a longer period |
|
lazy librarian |
generate and record the fine of overdue books |
keep track of overdue fines incurred by borrowers |
|
dutiful librarian |
record that a fine is paid |
keep track of accounting and prevent duplicate payments |
|
librarian |
view details of a book |
know more information about the book - author, genre, synopsis, etc |
|
careless librarian |
be able to undo a command |
undo my input mistakes |
|
careless librarian |
be able to redo a command |
undo my undo commands, in case I need it, without having to type out a possibly lengthy command |
|
health conscious, night-working librarian |
change the user interface into a night mode |
reduce the impact of light and glare on my eyes when I am working at night |
|
impatient librarian |
have my command inputs returned within 1 sec |
serve my customers quickly |
|
forgetful librarian |
look at the help section |
be reminded of the commands available |
|
helpful librarian |
be able to reserve a currently on-loan book |
allow borrowers to borrow the book once it is returned |
|
librarian |
be able to see an image of the book cover |
borrowers can know how the book looks like |
|
helpful librarian |
be able generate a list of most popular books |
recommend books to borrowers |
|
helpful librarian |
add a borrowers rating to the book |
recommend books based on ratings |
|
receptive librarian |
add a borrower’s review to the book |
recommend books based on reviews |
|
lazy librarian |
be able to auto-complete book title searches |
reduce my search time and give me nearby titles when I submit a book title query |
|
diligent librarian |
search for user profiles by name |
pull up his donate, borrowing, fine and payment history |
The use case diagram below illustrates the main use cases of LiBerry.
(For all use cases below, the System is LiBerry
and the Actor is the user
, who is a librarian, unless specified otherwise)
MSS
-
User searches for books by name, genre or author
-
LiBerry shows a list of books
-
User requests to delete a specific book in the list
-
LiBerry deletes the book
Use case ends.
Extensions
-
2a. The list is empty.
Use case ends
-
3a. The given index is invalid.
-
3a1. LiBerry shows an error message.
Use case resumes at step 2.
-
MSS
-
User adds a book by specifying its details
-
LiBerry shows a success message
Use case ends.
-
1a. The arguments provided are invalid.
-
1a1. LiBerry shows an error message.
Use case ends.
-
-
1b. Serial Number is not provided.
-
1b1. Serial Number is auto-generated.
Use case resumes at step 2.
-
MSS
-
Borrower comes to user to borrow a book.
-
User enters the borrower’s ID.
-
LiBerry shows that the borrower is being served.
-
User loans out the book to the borrower.
-
LiBerry shows the book as being successfully loaned out.
Use case ends.
Extensions
-
2a. LiBerry cannot find the ID in its system.
-
2a1. LiBerry requests for a valid and registered ID.
-
2a2. User enters new ID.
-
Steps 2a1-2a2 are repeated until the ID entered is valid.
Use case resumes at step 3.
-
-
3a. The book cannot be loaned out.
-
3a1. LiBerry shows an error message.
Use case ends.
-
-
*a. At any time, the user makes a typo in the input.
-
*a1. User undoes the last command entered.
-
*a2. User re-types the input.
Use case resumes at the step preceding this.
-
{More to be added}
-
Should work on any mainstream OS as long as it has Java
11
or above installed. -
Should be able to manage up to 20000 books, 5000 borrower records and 500000 loan records without a noticeable sluggishness in performance for typical usage.
-
A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.
{More to be added}
Given below are instructions to test the app manually.
ℹ️
|
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.
-
{ more test cases … }
-
Deleting a book while there are books are listed
-
Prerequisites: Books are displayed in the list on the UI.
-
Test case:
delete 1
Expected: First book is deleted from the list. Details of the deleted book shown in the status message. Timestamp in the status bar is updated. -
Test case:
delete 0
Expected: No book is deleted. Error details shown in the status message. Status bar remains the same. -
Other incorrect delete commands to try:
delete
,delete x
(where x is larger than the list size) {give more}
Expected: Similar to previous.
-
{ more test cases … }