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, and deletealias 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:

      • Added instructions for setting up CircleCI: [#117]

      • Changed the stylesheet used by asciidoc to generate documentation: [#55]

    • Community:

    • Tools:

      • Integrated a third party library (AsyncHttpClient) to the project: [#58]

      • Integrated a third party testing framework (Mockito) to the project: [#88]

      • Integrated CircleCI, TravisCI, AppVeyor, Coveralls, Codacy, and Netlify to the team repo

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

  • Adds a command alias for the specified COMMAND.

  • COMMAND should refer to a default, built-in command, and can optionally include command parameters.

  • ALIAS_NAME is case-insensitive, and must not contain any spaces or tabs.

  • If there is an existing alias with the same name as ALIAS_NAME, the existing alias will be overwritten.

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 name rm.
    You can then use rm INDEX in place of delete INDEX.

  • addalias read cmd/edit s/read p/none
    Adds a command alias with the name read.
    You can then use read INDEX in place of edit 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

  • Lists all command 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.

AliasesCommand

Delete a command alias : deletealias (since v1.5)

If you no longer require a command alias, you can remove it using deletealias.
Format: deletealias ALIAS_NAME

  • Deletes the command alias specified by the ALIAS_NAME.

  • ALIAS_NAME is case-insensitive, and must match the name of an existing alias.

Examples:

  • deletealias rm
    Deletes the command alias with the name rm.

  • deletealias read
    Deletes the command alias with the name read.

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:

SearchBooksSequenceDiagram

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:

SearchBooksActivityDiagram

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:

SearchCommandSequenceDiagram

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 return CompletableFuture 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 from MainApp all the way into each Command.

  • 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.

AliasSampleCommand

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.

AliasSampleCommand2

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.

AliasApplication

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.