PROJECT: Budget Buddy

Overview

Budget Buddy is a desktop expense tracker application that allows users to track their expenses. The main method of input by the user is using a command-line interface (CLI), but the application presents information using a graphical interface. It targets users from the School of Computing in the National University of Singapore, who are generally comfortable with command-line interfaces.

Budget Buddy has the usual requisite features of an expense tracker: transactions, accounts, categories, loan tracking and a loan splitter. Because we are targetting computing students, Budget Buddy also has a scripting engine, allowing users to write scripts that perform complex operations and extend the functionality of the application.

Budget Buddy benefits users by allowing them to know what they are spending on, so they can make informed decisions to change their spending behaviour and achieve their financial goals.

Budget Buddy is based on the AddressBook-Level3 application by SE-EDU, and currently consists of about 15 thousand lines of code.

My main contributions are the scripting engine, as well as managing releases and integrating contributions from other team members.

Summary of contributions

This section summarises the contributions I made to Budget Buddy.

  • Major enhancement: added a scripting engine and library, as well as script bindings for accounts, transactions and loans

    • What: This feature provides the user the ability to evaluate arbitrary scripts which have full access to the data and user interface of the application, and save these scripts in a library for convenient reuse.

    • Why: This feature allows the user to perform complex operations that may be inconvenient or impossible to do through the CLI, and extend the application with new commands and features on their own, without having to modify the application’s source code directly.

    • Highlights: This feature required thought to provide a ergonomic scripting interface to allow scripts to do common operations easily, without restricting scripts from doing more complex operations.

    • Credits: This feature is implemented using the Nashorn JavaScript engine, bundled with Java 11.

  • Minor enhancement: enhanced the command result mechanism to allow continuations, such as showing a file picker and performing actions after that; see PR #170.

  • Code contributed: See the RepoSense report here.

  • Other contributions:

    • Project management:

      • Managed releases v1.1 to v1.4 (4 releases) on GitHub

    • Community:

      • Helped to rebase and merge PRs, many with non-trivial merge conflicts, including PRs #55, #66, #85, #157, and #162

    • Tools:

      • Integrated Netlify, Coveralls and Codacy to the team repository

Contributions to the User Guide

This section includes the parts of the user guide that I wrote, to demonstrate my ability to write end-user facing documentation.

Scripting: script

scriptpanel
Figure 1. Scripts in the script tab

The scripting engine allows you to evaluate arbitrary scripts to perform complex operations on your transaction data, as well as extend the application and add commands and features of your own.

The scripting language is JavaScript (specifically, ECMAScript 5.1). For more details on the scripting environment and API, see here.

Scripts have full access to the application’s internals, as well as all Java standard library classes and APIs. It is possible to corrupt the application state by execution of a malicious or buggy script. There are no guarantees on application behaviour if scripts are used.

Evaluate a script: script eval

Evaluates a script and displays the result, which is the last expression evaluated in the script.

Format: script eval <script>

Examples:

  • script eval 1

    Evaluates to: 1

  • addTxn(1000, 'out', 'Lunch')

    Adds an out-transaction for $10 with description "Lunch".

Add a stored script: script add

Stores a script for future invocation. If a script with the same name already exists, that script is replaced.

Format: script add <script name> [d/<description>] [p/<file path>|s/<script>]

  • Script names may contain only alphanumeric characters, underscores, and dashes.

  • Script names are case-sensitive.

  • The file path may be absolute or relative. If it is relative, it is relative to the current working directory of Budget Buddy.

    • If you launched Budget Buddy from the command line, the current working directory starts from the directory your shell was in.

    • If you launched Budget Buddy by double-clicking the JAR file, the current working directory is typically the directory the JAR file is in.

    • There is no command that changes the current working directory, but a script may have done that.

  • If neither a file path nor the script code is given, a file picker is opened for you to select the script file.

The script is not checked for correctness before it is stored. Any syntax errors will be reported only when the script is run.

Examples:

  • script add hello-world s/"Hello world!"

    Adds a script named hello-world that simply results in "Hello world!" being printed.

  • script add add-transport d/Adds today’s transport fare.

    Adds a script named add-transport with description "Adds today’s transport fare.". A file picker is opened for you to select the script file.

Delete a stored script: script delete

Deletes a previously-stored script.

Format: script delete <script name>

  • Script names are case-sensitive.

Run a stored script: script run

Runs a previously-stored script and displays the result, which is the last expression evaluated in the script.

Format: script run <script name> [<argument>]

  • Script names are case-sensitive.

  • The script is run in the current script environment, which may contain variables from previous scripts that have run.

  • The argument is the rest of the input after the script name, and is passed to the script as a single string.

Examples:

  • script run add-transport

    Runs the script named add-transport. argv is an empty array.

  • script run echo Hello world!

    Runs the script named echo, with argv[0] set to "Hello world!".

List stored scripts: script list

Lists stored scripts.

Format: script list

Reset the scripting environment: script reset

Resets the scripting environment. This clears all previously defined variables, and restores all global variables and functions that may have been modified by scripts.

Format: script reset

The following is part of the Script API guide that I wrote, to demonstrate my ability to write end-user facing documentation for power users. See here for the full script API guide.

Scripting API

The Nashorn scripting engine is used. All features and Java class access of the Nashorn engine are available. See the Nashorn documentation for more details.

Budget Buddy defines a few global variables and functions to ease script writing. These are documented below.

Due to the nature of JavaScript, scripts may modify these variables and functions. To reset the script environment, use the script reset command.

Functions

The following helper functions are predefined in the script environment.

Function signatures

Function signatures specified below are in the following format:

functionName(parameter1, parameter2, { optionalParameter1, optionalParameter2 }) → ReturnType

Optional arguments should be given as a single JavaScript object. For example, for the above signature:

functionName("argument 1", "argument 2", { optionalParameter1: "optional argument 1" })

Types

  • If the type starts with a capital letter e.g. Account, it is a Java type.

  • If the type starts with a lowercase letter e.g. number, it is a JavaScript primitive.

  • If the type is [Type], it is an array of Type.

  • If a function’s return type is unspecified, the function does not return a value.

Account functions
  • addAccount(name, description) → Account

    Adds an account with the given name and description.

    Parameters:

    • name: the name of the account, as a string

    • description: the description of the account, as a string


  • morphAccount(oldAccount, { name, description }) → Account

    Returns a new Account that is the same as oldAccount except for the specified properties changed.

    Parameters:

    • oldAccount: the Account to morph

    • The remaining parameters are as in addAccount.

    • If a parameter is not specified, then the associated property is not changed.

Contributions to the Developer Guide

This section includes the parts of the developer guide that I wrote, to demonstrate my ability to write technical documentation and the technical depth of my contributions to the project.

Scripting

The scripting feature allows users to write scripts to automate tasks and extend the functionality of Budget Buddy.

Implementation

The script engine works independently of the rest of the application. At its core, it uses the Nashorn ECMAScript 5.1 engine bundled with Java 11 to evaluate scripts.

A set of convenience functions are provided to make basic tasks, such as manipulating transactions and accounts, easier. The full model is nevertheless exposed to scripts, and scripts are able to access any classes provided in the Java 11 standard library, as well as any dependencies included in the application.

There is a simple mechanism to store scripts to be run in future. This works together with rules to give the ability to have complex predicates and actions outside of those supported inherently by the program.

When the proposed alias feature is implemented, scripts and aliases can be used together to, in effect, allow users to create custom commands.

The following class diagram illustrates the design of the script engine and model. For clarity, parts of the model and logic components that do not interact directly with the script engine are omitted.

ScriptsClassDiagram
Figure 2. Structure of scripts in the model and logic components
Script engine

The logic component of the scripts feature contains the ScriptEngine class which underpins the entire scripting component. At its heart, the ScriptEngine class is simply a thin layer to abstract away the underlying script engine (in our case, Nashorn). It contains a method evaluateScript that accepts a string of script code, and arguments for the script, and then evaluates the script. There is an overload of evaluateScript that accepts a Script from the script model, which is simply a convenience method. There is no other relationship between the logic and model components.

The logic component also contains methods setVariable, addToEnvironment and resetEnvironment that allow functions and variables to be set in the script environment, to provide helper functions to the scripts, like those in the Scripting API.

To allow other components to add helper functions to the script environment, without the script engine having to be aware of those components, the ScriptEngine accepts ScriptEnvironmentInitialisers. A ScriptEnvironmentInitialiser is a FunctionalInterface that simply accepts a ScriptEngine and does some setup on it e.g. setting variables and functions using ScriptEngine#setVariable. ScriptEngine maintains a list of ScriptEnvironmentInitialisers and re-applies them each time ScriptEngine#resetEnvironment is called.

Library and storage

The model component of the scripts feature is a simple script library that allows users to store scripts with a name and a description for repeated use. See the interface ScriptLibrary and its implementation ScriptLibraryManager.

Scripts are persisted as individual files under the scripts/ directory in Budget Buddy’s data directory. Script names are not stored separately, but simply used as the file name of the scripts with a .js extension. See FlatfileScriptsStorage.

The script descriptions are stored in a separate descriptions.json file.

User interface

The last component of the scripts feature is the user interface.

The script engine is presented to the user as a set of simple commands. script eval and script run run scripts, while script reset resets the script environment. script add, script list and script delete manage the script library.

The following sequence diagram shows the interaction between the different components when the user runs a script from the script library using script run my-script.

ScriptsSequenceDiagram
Figure 3. Interaction between the components during the execution of script run my-script

The command is parsed into ScriptRunCommand by the command line parser. ScriptRunCommand then retrieves the Script from the ScriptLibraryManager and then hands the Script to ScriptEngine for evaluation.

Script bindings

Script bindings are added via ScriptEnvironmentInitialisers, which are registed with the ScriptEngine through Logic#addToScriptEnvironment.

Most of the functions exposed in the Scripting API come from the script-model bindings, in ScriptModelBinding. JavaFX-specific bindings are added in ScriptUiBinding, and bindings that require access to MainWindow internals are added in a private inner class of MainWindow.

Methods in the binding classes are added into the script environment by casting their method references to appropriate FunctionalInterfaces. Nashorn allows objects which implement a FunctionalInterface to be called as if they were functions, providing an ergonomic scripting experience for the user. Nashorn also helps to do some type conversion from JavaScript types to Java types, saving on the amount of boilerplate required in our implementation.

We declare an entire suite of functional interfaces in ScriptBindingInterfaces.

We do not use the functional interfaces provided in the Java package java.util.function because of these reasons:

  • These interfaces cannot be used if the methods throw any checked exceptions. Many script methods involve parsing strings into appropriate model types, which means that parsing exceptions need to be thrown, but they are checked exceptions.

  • These interfaces have generic type parameters. However, because Java implements generics using type erasure, it is not possible for Nashorn to determine the actual types of parameters, in order to do automatic conversion between JavaScript and Java types, if functional interfaces with generic type parameters were used. (They would all appear as Object at runtime.) This is also the reason that we do not use generics in the functional interfaces in ScriptBindingInterfaces.

Design considerations

Aspect: JavaScript engine

Two JavaScript engines were considered.

  • Alternative 1 (current choice): Use the Nashorn JavaScript engine.

    • Pros: It is bundled with Java 11.

    • Cons: It is deprecated and marked for removal in a future Java version, and it also interprets scripts, so it is slower than GraalVM.

  • Alternative 2: Use the GraalVM engine.

    • Pros: It is well-maintained, and it also just-in-time (JIT) compiles scripts, so it will be faster than Nashorn.

    • Cons: It is not bundled with Java 11, so it is an additional dependency.

We chose Nashorn because:

  • While it is slower, it is not too slow for the kinds of lightweight scripting tasks in Budget Buddy.

  • While it is deprecated, we are targetting specifically Java 11, so its future removal is not a concern at this moment.

  • It is bundled with Java 11, so we do not need to manage and ship an additional dependency.

Aspect: Script bindings
  • Alternative 1 (current choice): Use functional interfaces.

    • Pros: It requires less marshalling code in Budget Buddy, because Nashorn helps to perform most of the type conversion.

    • Cons: Many functional interfaces need to be declared for every unique method signature exposed to the script environment.

  • Alternative 2: Extend Nashorn’s AbstractJSObject, overriding the call method.

    • Pros: No extra declarations (e.g. functional interfaces) need to be made if a new method signature is to be exposed to the script environment.

    • Cons: Nashorn’s built-in type conversion and marshalling are not applicable; it has to be done manually in our AbstractJSObject subclass.

[Proposed] Aliases

The alias is a simple hook into the command parsing engine. If there is no built-in command corresponding to a command line, then the alias map is checked. If there is a matching alias, then the alias name in the command line is replaced, and the command execution is re-tried.

To prevent alias loops where the user creates an alias x mapping to y, and an alias y mapping to x, we track the aliases that have been applied, and stop evaluation if we see that the same alias has been applied more than once.

The following activity diagram illustrates the above algorithm.

AliasActivityDiagram
Figure 4. Alias resolution algorithm