PROJECT: Bibliotek
This portfolio page showcases my contributions to the project, Bibliotek. To view my full portfolio, please visit danielteo.me.
Overview
Bibliotek is a free book cataloguing application that is simple, fast, and easy to use. It is targeted at heavy readers who require a hassle-free way of managing and keeping track of their read and unread books.
Bibliotek has a GUI created with JavaFX, and is written in Java, with approximately 15 kLoC.
Summary of contributions
-
Major enhancement: added the ability to search for books online: [#58], [#62]
-
What it does: Allows the user to search for books on Google Books.
-
Justification: This feature improves the product because it provides the user an easy way to look up details about a book, or to find new books. It also lays the groundwork for the updated
add
command, which no longer requires the user to enter all the fields manually. -
Highlights: This enhancement required the addition of a new component (network component) that supports making HTTP requests to Google Book API endpoints, as well as parsing the JSON responses from these endpoints. Thus it required an in-depth analysis of design alternatives. The implementation also required the use of multi-threading to ensure that the UI remains responsive to the user. Lastly, this enhancement adds a new command
search
to allow the user to search for books. -
Credits: HTTP requests are made using AsyncHttpClient, and parsing of JSON results are accomplished with the help of Jackson.
-
-
Minor enhancement: added the ability to add, view, and delete custom command aliases: [#150]
-
Highlights: This enhancement adds three new commands
addalias
,aliases
, anddeletealias
to add, view, and delete custom command aliases. It also updates the command parser to support the use of command aliases, and the storage component to enable saving the command aliases in XML format.
-
-
Code contributed: [Functional code] [Test code]
-
Other contributions:
-
Project management:
-
Managed releases
v1.1
-v1.5rc
(5 releases) on GitHub
-
-
Product enhancements:
-
Morphed the original contact management app into a book management app: [#45], [#49], [#59]
-
Replaced the default browser panel with a panel to display book details [#73]
-
Added two new themes, and a
theme
command to allow changing themes: [#74], [#80] -
Improved
list
command to allow filtering and sorting: [#100]
-
-
Documentation:
-
Community:
-
Tools:
-
Contributions to the User Guide
Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users. |
Adding a command alias : addalias
(since v1.5)
If there is a command that you use frequently, and you find typing out the entire command to be too tedious,
you can add a command alias to reduce the amount of typing needed.
Format: addalias ALIAS_NAME cmd/COMMAND
If COMMAND does not specify a valid built-in command, you will
get an Unknown command message when you attempt to use the command alias.
|
You can use command aliases to specify default named parameters (parameters with a prefix, such as t/TITLE ).For example, if you want a custom list command that sorts by rating by default,
you can add a command alias using addalias ls cmd/list by/rating .You can override this default sort mode by specifying a different sort mode, e.g. ls by/status .
|
Examples:
-
addalias rm cmd/delete
Adds a command alias with the namerm
.
You can then userm INDEX
in place ofdelete INDEX
. -
addalias read cmd/edit s/read p/none
Adds a command alias with the nameread
.
You can then useread INDEX
in place ofedit INDEX s/read p/none
.
Listing command aliases : aliases
(since v1.5)
If you have forgotten some of your command aliases and need a quick refresher, you can use
the aliases
command to view them.
Format: aliases
After entering the aliases
command, Bibliotek shows Listed xx aliases.
to indicate that the command was successful.
The right panel will display a list of all your command aliases.
Contributions to the Developer Guide
Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project. |
Search feature
The search feature allows the user to search for books on Google Books that matches some user-specified parameters.
This allows the user to easily search for particular books, and to add them without having to enter all the information manually (using the add
command).
Current Implementation
Network component
As part of the implementation of the search feature, the network component was added to allow for communicating with online services. An overview of the network component can be found in the Developer Guide.
The Network
object is passed by LogicManager
into each Command
, and is available for use by default, without the need for the Command
to override setData
.
The Network
API exposes various methods for making web API calls to various endpoints.
Apart from those, it also provides a stop
method that allows for graceful shutdown of the internal asynchronous HTTP client.
As an example, if a command needs to retrieve search results from Google Books API, it can make a call to the searchBooks
method of the Network
API.
The following sequence diagram shows what happens when searchBooks
is called:
The methods shown above are asynchronous - they will not wait for the completion of the HTTP request.
In particular, note that when Network#searchBooks
finishes execution, the HTTP request might not have completed yet.
This is accomplished through the use of a chain of CompletableFuture
objects, which holds the operations that the above methods wish to apply to the data.
This is most evident in the executeGetAndApply
method, as shown below:
private <T> CompletableFuture<T> executeGetAndApply(String url, Function<String, ? extends T> fn) {
return httpClient
.makeGetRequest(url)
.thenApply(GoogleBooksApi::requireJsonContentType)
.thenApply(GoogleBooksApi::requireHttpStatusOk)
.thenApply(HttpResponse::getResponseBody)
.thenApply(fn);
}
Once the HTTP request completes, the operations in the CompletableFuture
objects will be executed. These operations are summarized by the following activity diagram:
If the HTTP request fails, the response is unexpected, or the conversion to BookShelf
fails, then the proceeding operations
added by the calls to thenApply
will be skipped, and the CompletableFuture
is considered to have completed exceptionally.
If necessary, the caller can handle the failure by chaining an exceptionally
call onto the CompletableFuture
it receives.
Search command
With the network component in place, the search
command can now be implemented.
When a search
command is entered, a SearchCommand
object will be created if the parsing of the command was successful,
which will make a call to searchBooks
on the Network
API, as shown in the sequence diagram below:
As described in Network component, when the event is handled by NetworkManager
, this will result in an asynchronous HTTP request being made to Google Books API.
Once the request and the parsing of the response completes successfully, the operation added by the thenAccept
call in SearchCommand
will be executed.
This results in the execution of the following method in SearchCommand
:
private void displaySearchResults(ReadOnlyBookShelf bookShelf) {
model.updateSearchResults(bookShelf);
model.setActiveListType(ActiveListType.SEARCH_RESULTS);
EventsCenter.getInstance().post(new ActiveListChangedEvent());
EventsCenter.getInstance().post(new NewResultAvailableEvent(
String.format(SearchCommand.MESSAGE_SEARCH_SUCCESS, bookShelf.size())));
EventsCenter.getInstance().post(new EnableCommandBoxRequestEvent());
}
The above method is run on the JavaFX thread (using Platform#runLater ) because it will result in updates to the book list panel.
If such updates are not done on the JavaFX thread, JavaFX will throw an IllegalStateException .
|
Design Considerations
Aspect: Asynchronous vs synchronous
-
Alternative 1 (current choice): HTTP requests are made asynchronously.
-
Pros: The application will be more responsive, as potentially long-running HTTP requests will not block the application thread.
-
Cons: Not straightforward to implement, especially considering that changes to the UI have to be made on the JavaFX application thread.
-
-
Alternative 2: HTTP requests are made synchronously (on the JavaFX application thread).
-
Pros: More straightforward to implement, as well as to understand the implementation.
-
Cons: The UI will be unresponsive for the duration of the HTTP requests, and this can degrade the user experience.
-
Aspect: Design of network API
-
Alternative 1 (current choice): Call methods on the
Network
API directly, which returnCompletableFuture
objects.-
Pros: More explicit flow of data, making it easier to understand and debug.
-
Cons: Since web API calls are made by certain commands, the
NetworkManager
will have to be passed fromMainApp
all the way into eachCommand
.
-
-
Alternative 2: Use events to request for web API calls and retrieve the results.
-
Pros: Less coupling - no component will be directly depending on the network component.
-
Cons: The flow of data can become less explicit and clear, and it becomes more complicated to use a single web API call for multiple purposes.
-
Aspect: Converting JSON responses to model types
-
Alternative 1 (current choice): Convert to a temporary data holder before converting to model type.
-
Pros: Easier and more straightforward implementation - a large part of the conversion work is done by the Jackson library.
-
Cons: Slower and less efficient - due to the double conversion and the use of the Reflection API (in the Jackson library).
-
-
Alternative 2: Convert parsed JSON directly to model type.
-
Pros: Faster and more efficient.
-
Cons: Code will be more complicated and tedious - we will need to traverse through the JSON node tree manually.
-
Command aliasing
The command aliasing system allows the user to specify short command aliases to use in place of the original commands. This improves the user experience by reducing the amount of typing the user needs to do.
Current Implementation
The user can customize their command aliases using the addalias
and deletealias
commands.
These commands allow the user to add and delete aliases respectively.
Adding command aliases
When the user attempts to add a new command alias using addalias ALIAS_NAME cmd/COMMAND
, the user input will first be
validated and pre-processed by AddAliasCommandParser
.
AddAliasCommandParser
verifies that both the ALIAS_NAME
and COMMAND
are non-empty, before
searching the COMMAND
for the first occurrence of an empty space that is followed by a named command parameter.
Using this point, the parser will split the COMMAND
into two parts - the prefix, and the named arguments.
The figure below shows an example for the edit 12 s/reading p/high
command.
The searching and splitting is accomplished using the following regular expression.
private static final Pattern COMMAND_FORMAT = Pattern.compile("(?<prefix>((?! \\w+\\/.*)[\\S ])+)(?<namedArgs>.*)");
Following this pre-processing step, a new Alias
will be created using the alias name, command prefix, and named arguments.
private static Alias createAlias(String aliasName, Matcher commandMatcher) {
return new Alias(aliasName, commandMatcher.group("prefix"), commandMatcher.group("namedArgs"));
}
The Alias
is then added to the UniqueAliasList
in ModelManager
, and this update will cause an AliasListChangedEvent
to be posted.
The event is handled by StorageManager
, which saves the updated alias list to data/aliaslist.xml
.
There are currently no checks in place to prevent the user from adding a command alias that refers to an invalid or non-existent built-in command. |
Using command aliases
The user can use command aliases in the same way they use normal commands. In this section, we assume that the user added rd
as an alias for edit s/reading
.
Whenever a command is entered into the command box, the command will be passed by LogicManager
into BookShelfParser#applyCommandAlias
,
where an attempt will be made to apply a matching command alias.
The application of a command alias begins by splitting the input into three parts - the alias name (first word in the input), unnamed arguments, and named arguments.
The figure below shows an example for the rd 12 p/high
command.
This splitting is accomplished using the following regular expression.
private static final Pattern ALIASED_COMMAND_FORMAT =
Pattern.compile(" *(?<aliasName>\\S+)(?<unnamedArgs>((?! [\\w]+\\/.*)[\\S ])*)(?<namedArgs>.*)");
If the alias name matches that of an existing alias, then the alias will be applied.
Otherwise, the input will be returned unchanged. In this case, the alias named rd
will be applied.
The result of the application of the alias is a new command that is formed by splicing parts of the user input into the aliased command.
Using rd 12 p/high
as the user input, this process is demonstrated in the figure below.
The resulting command, edit 12 s/reading p/high
, will then be parsed and executed, instead of the original user input.
Design Considerations
Aspect: Inclusion of command parameters in aliases
-
Alternative 1 (current choice): Allow aliasing of command parameters, together with the command word.
-
Pros: Increases the usefulness of the command aliasing system.
-
Cons: Less straightforward to implement, especially considering that some commands may require an index that comes before the named parameters.
-
-
Alternative 2: Only allow command words to be aliased.
-
Pros: More straightforward to implement.
-
Cons: Limits the usefulness of the command aliasing system, especially for users who may need to use certain commands that contain parameters regularly, e.g.
list s/high by/status
.
-