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:
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
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>]
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>
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>]
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
, withargv[0]
set to"Hello world!"
.
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.
Account functions
-
addAccount(name, description) → Account
Adds an account with the given name and description.
Parameters:
-
name
: the name of the account, as astring
-
description
: the description of the account, as astring
-
-
morphAccount(oldAccount, { name, description }) → Account
Returns a new
Account
that is the same asoldAccount
except for the specified properties changed.Parameters:
-
oldAccount
: theAccount
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.
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 ScriptEnvironmentInitialiser
s. 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 ScriptEnvironmentInitialiser
s 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
.
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
ScriptEnvironmentInitialiser
s, 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 FunctionalInterface
s. 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 inScriptBindingInterfaces
.
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 thecall
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.