
CUBA Home Accounting

The purpose of this article is to talk about the capabilities of the CUBA platform using the example of creating a small useful application.
CUBA is intended for fast development of business applications in Java, we already wrote about it several articles on Habré.
Typically, a platform builds either real, but too large and closed information systems, or applications in the style of “Hello World” or artificial examples like “Libraries” on our website. Therefore, some time ago, I decided to try to kill two birds with one stone - write a useful application for myself and put it in the public domain as an example of using our platform, since the subject area is simple and understandable to everyone.
What happened as a result
In short, the application solves two main tasks:
- At any point in time, it shows the current balance for all types of cash: cash, cards, deposits, debts, etc.
- Generates a report by categories of income and expenses, allowing you to find out what the money was spent on or where it came from in a certain period.
In a little more detail:
- Different types of cash are presented by accounts .
- Operations are possible on arrival to the account, expense from the account and transfer of funds between accounts.
- In the income or expense transaction, you can specify a category to specify where the money came from or what it was spent on.
- The balance of all accounts for the current date is constantly displayed and recalculated after each transaction.
- The report by categories of income and expenses shows a summary of two arbitrary periods at the same time for quick visual comparison. Any category can be excluded from comparison. For each line of the report, you can “fail” in the operation to see what it consists of.
- The system consists of three web applications deployed on one Tomcat:
- Middleware
- Full-featured UI on CUBA
- Responsive UI on Backbone.js + Bootstrap for ease of entering operations on mobile devices.
It looks somewhat redundant to solve such a simple task, but, firstly, the application was created more for educational purposes than practical, and secondly, it does not need much resources - my own copy easily runs on the Amazon EC2 micro-instance.
Some screenshots
Primary UI: list of operations

Main UI: report by categories of income / expenses

Responsive UI: list of operations

Responsive UI: current balance

How to start
The source code of the project is here: github.com/knstvk/akkount (KK - these are my initials, nothing better came to my mind).
The platform itself is not free, but five simultaneous connections in a free license are more than enough for home use, so if someone wants to use it, please.
It requires only JDK 7+ and the set JAVA_HOME environment variable. To build, open the command line in the root of the project and run
gradlew setupTomcat deploy
Gradle, which downloads the
After that, you need to start the HSQL server and create a database in the project data subdirectory:
gradlew startDb
gradlew createDb
To start Tomcat, you can use the Gradle command
gradlew start
or scripts
startup.*
in the subdirectory build/tomcat/bin
. The main web interface of the application is available at
localhost:8080/app
, responsive UI - at localhost:8080/app-portal
. The user is admin, the password is admin. The database is initially empty; there is a generator for filling it with test data. It is available through the Administration menu -> JMX Console -> app-core.akkount -> app-core.akkount: type = SampleDataGenerator . There is a method here
generateSampleData()
, which takes an integer as an input - the number of days ago from the current date for which you need to create operations. Enter 200, for example, and click Run. Wait for the operation to complete, then log out (the icon in the upper right corner) and log in again. You will see about the same as in my screenshots.How to look inside
To study and refine the application, I recommend downloading and installing CUBA Studio, IntelliJ IDEA and the CUBA plugin for it.
Further, I will not dwell on how and what is being done in the Studio. Everything is visual there, there is contextual help, there are video materials and documentation on the platform. I will explain the only nuance using the HSQL database: When opening a project using HSQL DB, the studio launches its own server on port 9001 and stores the databases in a directory
~/.haulmont/studio/hsqldb
. This means that if you started the HSQL server separately from the Studio using Gradle commands, you need to stop it. Database files, if necessary, can simply be transferred from data/akk
to ~/.haulmont/studio/hsqldb/akk
.In general, the application can also be run on a more serious database - PostgreSQL, Microsoft SQL Server or Oracle. To do this, in Studio just select the type of database you need in Project properties , then run Entities -> Generate DB Scripts , then in the main menu Run -> Create database .
The main objective of this article is to show those development techniques on the platform that are not visible in the Studio’s interface and which are difficult to find in the documentation if you don’t know in advance what to look for. Therefore, the description of the project will be fragmentary, with emphasis on non-obvious and non-standard things.
Data model

Entity classes are located in a module
global
that is accessible to both the middle layer and web clients. These are basically ordinary JPA entities, appropriately annotated and registered with
persistence.xml
. Most of them also have a CUBA-specific annotation @NamePattern
that defines the “instance name” - how to display a specific entity instance in the UI, something like that toString()
. If such an annotation is not specified, then just the toString()
class name and the object identifier are used as the instance name. Another specific annotation is @Listeners
, defines classes of listeners for creating / modifying objects. Entity listeners will be discussed in detail below. In addition to JPA entities, the project has a non-persistent entity
CategoryAmount
. Instances of non-persistent entities are not stored in the database, but are used only for transferring data between application layers and display with standard UI components. In this case, such an entity is used to generate a report by category: on the middle layer, data is extracted, instances are created and populated CategoryAmount
, and in the web client, these instances are placed in datasources and displayed in tables. Standard components Table
do not know anything about the origin of entities - for them, these are just objects that are known in the application’s metadata. And to include a non-persistent entity in the metadata, you need to add an annotation to its class, an annotation to its @MetaClass
attributes @MetaProperty
, and register the class in a filemetadata.xml
. Persistent entities, of course, are also described in metadata - for this, the metadata loader at the start of the application also parses the file persistence.xml
. Next to entities are enumeration classes, for example
OperationType
. Enumerations used in the data model in entity attributes are not quite ordinary: they implement an interface EnumClass
and have a field id
. Thus, the value stored in the database is separated from the Java value. This makes it possible to ensure compatibility with data in production DB during arbitrary refactoring of application code. In files
messages.properties
andmessages_ru.properties
An entity package contains localized names of entities and their attributes. These names are used in the UI if the visual components do not redefine them at their level. Message files are common UTF-8 encoded key-value sets. Searching for a message for a certain locale is similar to the rules PropertyResourceBundle
- first the key is searched in files with a suffix corresponding to the locale, if not found, in files without a suffix. Consider the essence of the model.
Currency
- currency. It has a unique code and an arbitrary name. The uniqueness of the currency code is supported by a unique index, which the Studio includes in the database creation scripts if the annotation@Column
contains a propertyunique = true
. The platform contains an exception handler thrown when uniqueness is violated in the database. This handler gives a standard message to the user. The handler can be replaced in your project.Account
- score. It has a unique name and an arbitrary description. It also contains a link to the currency and a separate currency code field. This field is an example of denormalization to improve performance. Since the lists of accounts are usually displayed together with the currency code, it makes sense to get rid of join in queries to the database by adding the currency code to the account itself. We will force the entity listener to update the currency code in the account when changing the currency of the account (although this is extremely rare in practice) - more on this later. The account also contains an attributeactive
- a sign that it is available for use in new operations, and an attributeincludeInTotal
- a sign that the balance of this account should be included in the total balance.Category
- category of income or expenses. It has a unique name and an arbitrary description. AttributecatType
- category type, determined by enumerationCategoryType
. As explained above, the value defined by the enumeration identifier (in this case the string “E” or “I”) is stored in the class field and in the database, and the getter and setter, and therefore the entire application code, work withCategoryType.INCOME
andCategoryType.EXPENSE
.Operation
- operation. Operation attributes: type (transferOperationType
), date, expense and income accounts (acc1
,acc2
) and corresponding amounts (amount1
,amount2
), category and comments.Balance
- account balance for some date. In general, for home accounting, it would be completely possible to do without this essence and calculate the balance always dynamically “from the beginning of time”: simply add up the entire income and take away all the expense on the account. But for fun, I decided to complicate the implementation in case of a large number of operations - the account balance at the beginning of each month is stored in copiesBalance
, when recording each operation, the balances at the beginning of the next month (and later, if any) are recounted. But to calculate the balance for the current date, you only need to take the balance at the beginning of the month and calculate the turnover for operations of the current month. This approach will not cause performance problems over time.UserData
- key-value storage of some user-related data. For example, last used account, report parameters by category. That is, it stores what needs to be “remembered” during repeated user actions. Possible keys are given by constants in the classUserDataKeys
.
Entity Listeners
If you worked with JPA, you probably used entity listeners as well. This is a convenient mechanism for performing any actions when saving changes to entities in the database. Most importantly, all changes made by the listeners are made in the same transaction - similar to database triggers. Therefore, it is convenient to organize logic for maintaining the consistency of a data model on listeners.
Entity listeners in CUBA are slightly different in implementation from JPA. The listener class must implement one or more special interfaces (
BeforeInsertEntityListener
, BeforeUpdateEntityListener
and others). Listeners are registered on the entity class in the annotation@Listeners
enumerating class names in an array of strings. You cannot use literals of the listener classes directly in the entity class, since the entity is a global object that is accessible to both the middle layer and clients, and the listener is an object of only the middle layer, inaccessible to clients. The listeners only live on the middle layer because they need access to EntityManager
other means of working with the database. In this application, entity listeners perform two functions: firstly, they update denormalized fields, and secondly, they recalculate account balances at the beginning of the month.
The first problem is trivial: the listener
AccountEntityListener
in the methods onBeforeInsert()
, onBeforeUpdate()
updates the value of the currency code. To do this, it is enough to refer to the associated instance Currency
.The second task is essentially one of the main tasks in the business logic of the application. Doing it
OperationEntityListener
in the way onBeforeInsert()
, onBeforeUpdate()
, onBeforeDelete()
. In addition to recounting the balance, this listener also remembers the UserData
last used accounts in the objects . It should be noted that in the Before-listener there are no restrictions on the use
EntityManager
, loading and modification of instances of any entities. For example, instances are loaded and modified addOperation()
using . They will be stored in the database at the same time as the operation in one transaction.
Sometimes in the listener it is required to get the “previous” state of the object that is now in the persistent context, that is, the state that is now in the database. For example, in this case, inQuery
Balance
onBeforeUpdate()
we need to first subtract from the balance the previous value of the transaction amount, and then add the new value. To do this, a getOldOperation()
new transaction is started in the method using persistence.createTransaction()
, in its context, another instance is obtained EntityManager
, and through it the previous state of the operation with the same identifier is loaded from the database. Then the new transaction is completed, without affecting the current one in which our listener is working.Middle layer components
The main work on loading data to the client level and saving user-made changes to the database is performed by the standard DataService implemented in the platform. Through it, data sources of visual components work. This is not enough in our application, so several specific services have been created.
Firstly, it
UserDataService
allows working with key-value storage UserData
, providing a typed interface for reading and writing entity identifiers. The service interface is in the module global
because it must be accessible to the client level. The service implementation is in the core module in the class UserDataServiceBean
. She delegates calls to binUserDataWorker
, in which the code that does the useful work is concentrated. This is done because this functionality is also required in OperationEntityListener
, that is, “from within” the middle layer. The service forms the “middleware border” and is intended only for calling from client blocks. It should not be called from within the middle layer components, as this leads to the repeated operation of the interceptor, which checks authentication and handles exceptions in a special way. And just to restore order, it’s worth separating the services that are called from outside the middleware from the rest of the beans that are called from the inside. If only because when calling from outside the transaction is always absent, and when calling from middleware code, the transaction can already be opened. Next service -
BalanceService
. It allows you to get the value of the account balance on an arbitrary date. Since this functionality is required by both clients in the UI and on the middle layer (test data generator), it is also placed in a separate bin BalanceWorker
. And the last service is
ReportService
. It retrieves the data for the report by category, and returns it in the form of a list of instances of a non-persistent entity CategoryAmount
. A bin is also implemented on the middle layer
SampleDataGenerator
which is intended to generate test data. For functionality of this kind, a complex UI is usually not required - it is enough to provide a call with the transfer of simple parameters, sometimes you need to display some state in the form of a set of attributes. In addition, only the administrator works with this, not the users of the system. In this case, it is convenient to give the bean a JMX interface and call its methods from the JMX console built into the web client, or by connecting to any external JMX tool. In our case, the bean has an interface SampleDataGeneratorMBean
, and it is registered in spring.xml
the core module. Note that the
generateSampleData()
bean method is annotated as@Authenticated
. Это означает, что при вызове данного метода будет выполнен специальный системный логин и в потоке выполнения будет присутствовать пользовательская сессия. Она требуется в данном случае потому, что метод создает и изменяет через EntityManager
сущности, которые при сохранении требуют установки их атрибутов createdBy
, updatedBy
— кто изменял данные экземпляры. С другой стороны, метод removeAllData()
, также вызываемый через JMX-интерфейс, не требует аутентификации потому, что он удаляет данные с помощью SQL-запросов через QueryRunner
и нигде не обращается к пользовательской сессии.In general, a mandatory check for the presence of a user session is performed only at the entrance to the middle layer from the client level - in the services interceptor. Check or not check the presence of the session and user rights at the middleware level - the application developer decides, but in some cases the presence of the session is necessary because of the need to put the username in the attributes of entity auditing. In addition, user rights are always checked in
DataWorker
- the bin, to which it DataService
delegates the execution of CRUD operations with entities.Main application window
The standard feature of the CUBA web client is a hidden panel on the left side of the application window, which usually displays the so-called “application folders” and “search folders”. These folders are used for quick access to information - clicking on a folder opens a specific screen with a list of entities and a filter applied.
It seemed logical to me to display information about the current balance in the left part of the main window. So I embedded the balance panel at the top of the folders panel.
This is done as follows:
- A
FoldersPane
class is inherited from the platformLeftPanel
, methodsinit()
andrefreshFolders()
, in which the method is called , are redefinedcreateBalancePanel()
. A new container is created in it, filled with data obtained fromBalanceService
, and placed at the top of the parent container. - To
LeftPanel
be used instead of the standard oneFoldersPane
, theAppWindow
class is inherited from the platformAkkAppWindow
and the method is redefinedcreateFoldersPane()
. - To be
AkkAppWindow
used instead of the standard oneAppWindow
, thecreateAppWindow()
class method is redefinedApp
. In addition, the method for accessing the new panelgetLeftPanel()
is defined here - it is called from the screens to update the balance after committing or deleting operations.
Operations browser
The screen descriptor is located in the file
operation-browse.xml
. Everything is standard here, except for the use of formatter classes to represent dates and amounts in the operation table. To display the date, a platform is used
DateFormatter
, to which the format is transmitted by key from the package of localized messages. Thus, the format string can be different for different languages - for Russian, the date is separated by dots, and for English - with /. In order for the amounts to be displayed without a fractional part, and 0 not to be displayed at all, a class has been created in the project
DecimalFormatter
- it is used in the columns of amounts.Operation editor
It is more interesting here: an operation can be one of three types (income, expense, transfer), and the editing screen should look different for them.
At first glance, the first two screens seem the same, but this is actually not the case: visual components work with different attributes of the entity
Operation
- expense with acc1
and amount1
, income with acc2
and amount2
. This variability could be implemented completely in the controller code, but I decided to do it more declaratively - by splitting the different parts of the screen into separate frames. Three frames - by the number of types of operations. All of them are located in the same package as the operation editing screen itself. Most often, frames are connected statically - using the component
iframe
in an XML screen descriptor. This does not suit us, since we need to choose the desired frame depending on the type of operation. Therefore, in the XML descriptor of the screen operation-edit.xml
, only the container for the frame is defined - the component groupBox
with the identifier frameContainer
, and the actual creation and insertion of the frame into the screen is performed in the controller OperationEdit
: @Inject
private GroupBoxLayout frameContainer;
private OperationFrame operationFrame;
@Override
public void init(Map params) {
...
String frameId = operation.getOpType().name().toLowerCase() + "-frame";
operationFrame = openFrame(frameContainer, frameId, params);
Here
OperationFrame
is the interface that the operation type frame controllers implement. Through it, it is convenient to uniformly manage all three frames - to initialize and validate them. There is another interesting point in the
init()
controller method OperationEdit
- a listener is registered that fires after an operation is committed: @Override
public void init(Map params) {
...
getDsContext().addListener(new DsContext.CommitListenerAdapter() {
@Override
public void afterCommit(CommitContext context, Set result) {
LeftPanel leftPanel = App.getLeftPanel();
if (leftPanel != null)
leftPanel.refreshBalance();
}
});
}
This listener updates the contents of the left panel displaying the current balance.
Operation type frames have the following common feature - text fields that work with sums are not attached to the data source. This is done so that you can enter an arithmetic expression in the field, and the system calculates the amount.
Consider
expense-frame.xml
. It declares a component textField
with an identifier amountField
. The controller ExpenseFrame
uses a bin AmountCalculator
in which the sum calculation logic is encapsulated: @Inject
private TextField amountField;
@Inject
private AmountCalculator amountCalculator;
@Override
public void postInit(Operation item) {
amountCalculator.initAmount(amountField, item.getAmount1());
…
}
@Override
public void postValidate(ValidationErrors errors) {
BigDecimal value = amountCalculator.calculateAmount(amountField, errors);
…
}
The same bean defined on the Web Client layer is used in two other frame controllers. The
initAmount()
bean method sets the current amount formatted by data type in the text box BigDecimal
. It is simply datatype = decimal
impossible to specify for the component, since in this case it will be possible to enter only a number in it, and we need to be able to enter arithmetic expressions. The method calculateAmount()
checks the expression for correctness using regexp, and then executes it as an expression on Groovy through the interface Scripting
. The result will be a number, which is returned to the screen controller for setting up the operation.Category Report

This interactive report is implemented by the screen
categories-report.xml
. It is interesting primarily because it contains two custom data sources of the type CategoryAmountDatasource
. The data source class is specified in the datasourceClass
element attribute collectionDatasource
. A JPQL operator is also specified for these data sources, however, it is not used and is present only because the Studio automatically generates the query text if it is not specified. In fact, the data source CategoryAmountDatasource
overrides the method loadData()
and, instead of loading data through DataService
a JPQL query, calls the service ReportService
, passing it the necessary parameters:public class CategoryAmountDatasource extends CollectionDatasourceImpl {
private ReportService service = AppBeans.get(ReportService.NAME);
@Override
protected void loadData(Map params) {
...
Date fromDate = (Date) params.get("from");
Date toDate = (Date) params.get("to");
...
List list = service.getTurnoverByCategories(fromDate, toDate, categoryType, currency.getCode(), ids);
for (CategoryAmount categoryAmount : list) {
data.put(categoryAmount.getId(), categoryAmount);
}
...
}
Parameters are set by the screen controller in the
refresh()
data source method - see methods refreshDs1()
, refreshDs2()
classes CategoriesReport
. The service returns a list of non-persistent entity instances CategoryAmount
, and the data source stores them in its data collection. Thus, the tables associated with these data sources display instances CategoryAmount
as any other entities loaded from the database in the usual way. The function of the Exclude button is interestingly designed , allowing you to remove the selected category from consideration. Two such buttons are declared
in the descriptor
categories-report.xml
- for the left and right tables. Each of the buttons is associated with an action.excludeCategory
your table. However, no actions were declared for tables in the XML descriptor. How does this work? The fact is that the actions for the tables in this case are added in the init()
screen controller method : see the method initExcludedCategories()
. This method also “recalls” the list of previously excluded categories remembered using the service UserDataService
. The type action,
ExcludeCategoryAction
when triggered, calls a method excludeCategory()
that ComponentsFactory
creates a container and an inscription with a link button corresponding to the excluded category and places the new container inside the container declared in advance in the handleexcludedBox
. For each button, a listener is created, when triggered, the entire container in which the button is located along with the label is removed from the parent container. In addition, data sources are updated by reorganizing category lists. In general, the category report screen is a rather non-standard option for using the platform, therefore there is a lot of manually written logic in it, which is usually hidden inside standard options for component interaction.
Acknowledgments
I got some ideas from the wonderful zenmoney.ru service , which I used for some time. All open-source libraries and frameworks that are part of the platform are listed in the Help -> About -> Credits window .
To be continued
In the next article about the same application, I plan to talk about the device block responsive UI, which is written in Backbone.js + Bootstrap and interacts with the middle layer through the REST API. In addition, I will try to slightly change the theme of the main UI and add it with a new UI component to illustrate the possibilities of customizing the interface in projects.