PROJECT: Infinity Machine


Overview

Infinity Machine is a source manager to be used by tech-savvy university students who are comfortable with the command line interface (CLI). It is an application for the efficient storage, management, and retrieval of research information (sources). It is privacy-focused, powerful, and flexible. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java.

Summary of contributions

  • Major enhancement: added the ability to add, remove, and manage command aliases

    • What it does: allows the user to define custom aliases for commands, which persist across sessions

    • Justification: This feature improves the product significantly because a user can: (1) create custom memorable alises for any of the existing commands; (2) work more efficiently by creating shortcuts for lengthy and commonly-used commands

    • Highlights: This enhancement affects the way user input is parsed and executed, both for existing commands and commands to be added in the future. It required an extensive analysis of various design approaches. The implementation was challenging because:

      • There are different classes of commands that are parsed and handled in different ways. This enhancement must be agnostic to these various classes of commands, and must potentially work with any new future classes.

      • This enhancement must both be backward-compatible with existing commands and also transparent to implementors of future commands. In other words, this enhancement will work seamlessly with any future command implementations, even if the implementors have no knowledge of this enhancement.

  • Minor enhancement: added a panic/unpanic mode command that allows the user to quickly hide the current list of sources from display.

  • Code contributed: [Reposense][AliasManager][ConcreteAliasManager][ConcreteAliasManagerTest][AliasStorage][ConcreteAliasStorage]

  • Other contributions:

    • Project management:

      • Co-managed releases v1.1 - v1.3 (3 releases) on GitHub

    • Enhancements to existing features:

      • Updated codebase to morph from AddressBook to Infinity Machine (Pull requests #89, #93, #98)

      • Included reposense (Pull request #117)

      • Updated existing tests to include integration tests with major enhancement (Pull request #132)

    • Community:

      • PRs reviewed (with non-trivial review comments): #54, #69

      • Contributed to forum discussions (examples: 1, 2, 3, 4, 5)

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.

Disabling panic mode : unpanic

Restores the user’s original data.
Format: unpanic

This reverses the effect of panic mode by restoring the user’s original data. The restorated is reflected on the disk too; the JSON file is reset to its original state and will now track the original data store.

Command aliases : alias

Note: Aliases do not work in recycle-bin mode.

Creating an alias: alias

Allows the user to create aliases create aliases for commands.
Format: alias COMMAND ALIAS

Examples:

  • alias count c (c is now a valid pseudo-command that works exactly like count)

  • alias invalid i (this doesn’t work because invalid is not a valid command)

As the above examples demonstrate, aliases may only be created for valid commands.

If the user attempts to add an alias that has already been added, the old one will be overwritten. For example:

  • alias count c

  • alias invalid c

c is now an alias for the invalid command invalid.

The command may not be another alias. The alias may not be a command.

  • alias count ct (ct is now an alias for count)

  • alias ct c (this is invalid because ct is another alias)

  • alias count list (this is invalid because list is a command)

The alias must be syntatically valid. A valid syntax may only contain alphabets.

  • alias list l (valid)

  • alias count ct (valid)

  • alias clear $ (invalid)

Removing an alias: alias-rm

Allows the user to remove previously-defined aliases.
Format: alias-rm ALIAS

Examples:

  • alias count c (c is now an alias for count)

  • alias-rm c (c is no longer an alias for count)

If the user attempts to remove a non-existent alias, nothing happens. alias-rm only guarantees that after it is performed, the alias argument does not exist.

Listing all aliases: alias-ls

Lists all defined aliases and their associated commands.
Format: alias-ls

Clearing all aliases: alias-clear

Clears all defined aliases and their associaetd commands.
Format: alias-clear

Alias persistence

Aliases are persistent across usage sessions. When an alias is created or removed, this is recorded to disk. No action is required on the user’s part.

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.

Panic Mode feature

The panic mode feature allows the user to temporarily hide user data and replace it with dummy data.

Overview

The user’s original data is replaced by dummy data for the duration that panic mode is enabled. Enabling panic mode can be thought of as "stashing" the user’s data temporarily in memory. This is reflected both on-screen and on-disk. On the screen, the list of sources is replaced by an empty dummy list. On disk, the contents of the JSON file storing the user’s sources is replaced by dummy content that tracks and reflects the dummy data.

Implementation

This is implemented by "swapping" the source manager with an empty dummy source manager. This "swap" is carried out by storing the original source manager in a private variable sourceManagerBackup, and then resetting the original source manager with a new empty source manager instance. We also set the boolean variable panicMode = true.

When the user disables panic mode, we restore the original source manager, and reset panicMode = false.

Elaboration

We use a boolean variable panicMode to keep track of whether panic mode has been activated. This is to guard against the scenario of entering panic mode while already in panic mode, which results in permanent data loss.

This is because when panic mode is activated, we store the original source manager in the private variable sourceManagerBackup, and reset the original source manager, as described above.

Therefore, should panic mode be activated while already in panic mode, sourceManagerBackup will now store the dummy source manager, and the original source manager will be deallocated and eventually purged from memory by Java’s garbage collector.

Since the JSON file on disk automatically tracks the source manager through the observer pattern, it automatically updates to track and reflect the data in the dummy source manager.

Command Alias feature

The command alias feature allows users to use shorthand commands to rapidly "get things done", for instance using a instead of add, or c instead of count.

Users may do one of the following:

  1. Add a new alias

  2. Remove an existing alias

  3. List all aliases

  4. Clear all aliases

In designing and implementing this feature, the overarching principle is to maximize transparency and compatibility. This means that it should be transparent to future developers/maintainers (they should not need to understand how this feature works, or be subject to any design constraints). It should also be backwards-compatible with existing commands (existing code should not be modified). This allows for maximum extensibility and maintainability.

Overview

This feature is backed by an in-memory database implemented as a Java HashMap<String, String>. A HashMap is chosen for the following reasons:

  • Adding and removing an alias is straightforward (using Java HashMap API) and efficient (O(1) time)

  • Checking whether an alias exists (membership) is efficient (O(1) time)

  • HashMaps naturally facilitate the process of associating a key-value pair

Alternative: No reasonable alternative implementations exist. For instance, using a Java ArrayList adds additional code complexity, as there needs to be a way of associating a key with a value. For instance, we could create an ArrayList<AliasWrapper>, where AliasWrapper is a wrapper class to associate 2 strings. However, that is inelegant and inefficient, as opposed to a HashMap solution. Furthermore, checking for membership in an ArrayList is an O(N) operation in an unsorted list, or O(log(N)) in a sorted list.

AliasManagerClassDiagram

Aliasing feature: implementation

Meta-commands are not implemented as regular commands. Regular commands inherit Command, and operate on the model (their main method is public CommandResult execute(Model model, CommandHistory history) throws CommandException {}). On the other hand, meta-commands operate on an AliasManager object. Therefore, it is desirable to draw a distinction between regular commands and meta-commands throughout the codebase.

To implement aliasing, we first create an AliasManager interface to practice design by contract. AliasManager is command-agnostic. It operates through its API (specified in the interface), and is not concerned with the choice of meta-commands (e.g. alias-rm vs alias-remove). We also created a class ConcreteAliasManager to implement the AliasManager interface.

As for SourceManagerParser, we created an alternative constructor to accept an AliasManager object to support dependency injection. Otherwise, the default constructor instantiates ConcreteAliasManager.

We chose to create the AliasManager interface to decouple SourceManagerParser and ConcreteAliasManager. In normal operation, we would always use ConcreteAliasManager. However, working through an interface (and implementing an alternative constructor) provides the flexibility to swap out ConcreteAliasManager for an alternative AliasManager implementation, such as a stub, for unit testing. This improves testability, maintainability, and extensibility.

To implement the meta-commands, we create an abstract superclass AliasMetaCommandParser that implements Parser<DummyCommand>. This serves as an alternative class of command parsers (for meta-commands), in contrast to the regular ones which are of the type Parser<? extends Command>. (As mentioned above, meta-commands are fundamentally different from regular commands, and it is desirable to maintain this distinction.) The key difference between the two is that an AliasMetaCommandParser has a field storing a reference to the AliasManager object which it requires to interact with (e.g. when adding/removing an alias).

Parsers are expected to return a Command object which SourceManagerParser returns in its parseCommand(String userInput) method. Typically, a Command object operates on the Model (e.g. AddCommand calls model.addSource()). However, meta-commands operate on the AliasManager, and not the model. Therefore, for this purpose, we created a class DummyCommand which nominally extends Command, but actually does nothing except return a CommandResult object to display feedback to the user. This promotes transparency and compatibility.

Finally, we create a CommandValidator interface. AliasManager uses the CommandValidator for two purposes:

  1. Validate a command before registering an alias to it

  2. Ensure that a command isn’t designated as an un-aliasable command

We chose this implementation and design pattern for several reasons:

  1. By designating an object as a CommandValidator, we are able to avoid hardcoding the list of valid and un-aliasable commands into AliasManager. This makes for a more reusable component and improves testability and maintainability. It also embodies the Open-Closed Principle.

  2. Typically, the SourceManagerParser (which by definition should know about the various valid commands) is the designated CommandValidator. However, the SourceManagerParser also has an association with the AliasManager. By creating an interface, we avoid a situation of circular dependency whereby both components are tightly coupled to each other.

Aliasing feature: operation

Meta-commands

When a meta-command is detected to have been entered, SourceManagerParser delegates it to the appropriate AliasMetaCommandParser to handle. For instance, alias FOO BAR is delegated to the AliasAddMetaCommandParser (a concrete subclass of AliasMetaCommandParser) with the arguments "FOO BAR". The appropriate AliasMetaCommandParser parses the arguments and returns a DummyCommand response object.

The following sequence diagram illustrates the operation of the "add alias" meta-command (assuming that valid user input is provided).

AliasManagerMetaCommandSequenceDiagram
This delegation design pattern is chosen for 2 reasons: Firstly, it hides complexity in SourceManagerParser by abstracting the logic of interacting with AliasManager away. This makes SourceManagerParser more readable, declarative, and maintainable. This also allows us to practice the Single Responsibility Principle and Single Layer of Abstraction Principle, among others. Secondly, it improves testability by facilitating unit testing of smaller blocks of logic, rather than a single giant block.

If user input is valid, the AliasMetaCommandParser, which stores a reference to the AliasManager object, operates on it through the AliasManager API.

Aliases

In normal operation, when the user enters an alias, SourceManagerParser parses the user input to extract the "command word". It checks whether the "command word" is a pre-existing alias using AliasManager’s isAlias() method. If so, it fetches the original command that the alias is associated to using AliasManager’s getCommand() method.

Finally, SourceManagerParser recursively calls itself once using the original command retrieved from AliasManager to execute the original command that the alias is associated with.

This sequence diagram provides a high-level overview of this operation. Finer-level details have been omitted.

AliasManagerSequenceDiagram
AliasManager doesn’t allow the aliasing of invalid commands, nor the aliasing of an alias. This is to guard against the risk of an infinite loop, e.g. where alias1 is the alias of alias2, which is the alias of alias1. With the current implementation, we can be assured that the recursion depth is at most 2.

Persistence feature: implementation

The usefulness of aliases would be significantly diminished if they do not persist between sessions. Therefore, we want aliases to be stored on disk and automatically loaded in future sessions on application startup.

To accomplish this, we create an AliasStorage interface, and an implementing class ConcreteAliasStorage. We also modify ConcreteAliasManager to accept an AliasStorage object during its instantiation. To facilitate unit testing, we allow a null AliasStorage object which disables data persistence.

The motivations for this design pattern is similar to the discussion above for creating the AliasManager interface. Essentially, we want to decouple components as much as possible, support dependency injection, and improve testability and maintainability.

ConcreteAliasStorage is responsible for reading/writing from/to disk, and therefore converting the in-memory database (HashMap object) of aliases into/from an encoded representation. When AliasManager’s aliases database is mutated (i.e. create or remove alias), it calls ConcreteAliasStorage’s saveAliases() method.

Alternative: A more elegant implementation would be to apply the observer pattern, with the observer observing the aliases HashMap database, and calling saveAliases() when it is mutated. However, given the simplicity of AliasManager, we believe that applying the observer pattern will result in unnecessary overhead, with minimal (or no) tangible benefits.

Within ConcreteAliasStorage, its saveAliases() method encodes aliases and commands into a string, in the following format: alias1:command1;alias2:command2;alias3:command3. Conversely, readAliases() parses this string and reconstructs the aliases HashMap database.

Alternative: We opted to use our own very simple encoding scheme instead of JSON. JSON is more suited for "document-like" objects with different properties, some of which are possibly nested multiple layers. However, in our case, we only have a series of key:value pairs, in a predictable form, with no nesting. Therefore, we thought that a simple semicolon-separated key:value pair encoding scheme would suffice.