gradlew setupTomcat deploy
gradlew startDb
gradlew createDb
gradlew start
startup.*
scripts in the build/tomcat/bin
subdirectory.localhost:8080/app
localhost:8080/app
, responsive UI - on localhost:8080/app-portal
localhost:8080/app-portal
. User is admin, password is admin.generateSampleData()
, which takes an integer as input - the number of days ago from the current date for which you need to create operations. For example, type 200, and click Start. 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 thing as in my screenshots.~/.haulmont/studio/hsqldb
. This means that if you started the HSQL server separately from Studio with the Gradle commands, you need to stop it. Database files, if necessary, can simply be transferred from data/akk
to ~/.haulmont/studio/hsqldb/akk
.global
module, which is accessible to both the middle layer and web clients.persistence.xml
. Most of them also have a CUBA-specific annotation @NamePattern
, which specifies “instance name” - how to display a specific entity instance in the UI, something like toString()
. If such an annotation is not specified, just toString()
used as the instance name, which returns the class name and object identifier. Another specific annotation - @Listeners
, defines classes of listeners for creating / changing objects. The entity listeners below will be discussed in detail.CategoryAmount
entity in the project. Instances of non-persistent entities are not stored in the database, but are used only for transferring data between application layers and displaying by standard UI components. In this case, this entity is used to generate a report by category: the middle layer retrieves the data, creates and populates CategoryAmount
instances, and in the web client these instances are put into data sources (datasources) and displayed in tables. Standard Table
components do not know anything about the origin of entities — for them, they are simply objects known in the application's metadata. And in order to include a non-persistent entity in metadata, you need to add the @MetaClass
annotation to its class, attributes the @MetaProperty
annotation to @MetaProperty
, and register the class in the metadata.xml
file. Of course, persistent entities are also described in metadata - for this, the metadata loader at the start of the application also parses the persistence.xml
file.OperationType
. Enumerations that are used in the data model in the attributes of entities are not quite ordinary: they implement the EnumClass
interface and have the id
field. 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 with arbitrary refactoring of application code.messages.properties
and messages_ru.properties
an entity package there are localized names of entities and their attributes. These names are used in the UI, if the visual components do not override them at their level. Message files are regular UTF-8 key-value sets. The search for a message for a locale is similar to the PropertyResourceBundle
rules — first, the key is searched for in files with a suffix that matches the locale, if not found — in files without a suffix.Currency
- currency. It has a unique code and an arbitrary name. The uniqueness of the currency code is maintained by a unique index, which the Studio includes in the database creation scripts if the @Column
annotation contains the unique = true
property. The platform contains an exception handler thrown in case of uniqueness violation in the database. This handler issues a standard message to the user. The handler can be changed in your project.Account
- an account. It has a unique name and an arbitrary description. It also contains a link to the currency and a separate field of the currency code. This field is an example of denormalization to improve performance. Since in the lists of accounts, as a rule, they are displayed together with the currency code, it makes sense to get rid of the join in database queries by adding the currency code to the account itself. We will force the entity listener to update the currency code in the account when the account currency changes (even though in practice it happens very rarely) - more on this later. The account also contains an active
attribute — a sign that it is available for use in new transactions, and an includeInTotal
attribute — a sign that the balance on this account should be included in the aggregate balance.Category
- the category of income or expenses. It has a unique name and an arbitrary description. The catType
attribute is a category type, defined by the CategoryType
enum. As already explained above, the class field and the database store the value determined by the enumeration identifier (in this case, the string “E” or “I”), and the getter and setter, and therefore the entire application code, work with the CategoryType.INCOME
values and CategoryType.EXPENSE
.Operation
- operation. Operation attributes: type (transfer OperationType
), date, expense and acc1
accounts ( acc1
, acc2
) and corresponding amounts ( amount1
, amount2
), category and comments.Balance
- the balance on the account on a certain date. In general, for home accounting it would be quite possible to do without this entity and to always calculate the balance dynamically “from the beginning of time”: just lay down the entire income and take away all the expense on the account. But I decided for interest to complicate the implementation in case of a large number of operations - the balance on the account at the beginning of each month is stored in Balance
instances, when recording each operation, balances for the beginning of the next month (and later, if any) are recalculated. 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 of the operations of the current month. This approach will not cause performance problems over time.UserData
- key-value storage of some data associated with the user. For example, the last used account, report parameters by category. That is, it stores what is necessary to “remember” when repeated actions of the user. Possible keys are defined as constants in the UserDataKeys
class.BeforeInsertEntityListener
, BeforeUpdateEntityListener
, etc.). Listeners on the entity class are registered in the @Listeners annotation @Listeners
listing the class names in the array of strings. You cannot use literals class literals directly in an entity class, since an entity is a global object that is accessible to both the middle layer and clients, and the listener is only an object of the middle layer that is inaccessible to clients. The listeners live only on the middle layer because they need access to the EntityManager
and other database tools.onBeforeInsert()
methods, onBeforeUpdate()
updates the value of the currency code. To do this, it is enough for him to refer to the associated instance of Currency
.OperationEntityListener
in the methods onBeforeInsert()
, onBeforeUpdate()
, onBeforeDelete()
. In addition to recalculating the balance, this listener also remembers the last used accounts in UserData
objects.EntityManager
, loading and modifying instances of any entities. For example, in addOperation()
using Query
Balance
instances are loaded and modified. They will be stored in the database simultaneously with the operation in a single transaction.onBeforeUpdate()
we must first subtract the previous value of the transaction amount from the balance, and then add the new value. To do this, in the getOldOperation()
method, a new transaction is started using persistence.createTransaction()
, in its context another EntityManager
obtained, and through it the previous operation state with the same identifier is loaded from the database. Then the new transaction is completed without affecting the current one, in which our listener operates.UserDataService
, which allows you to work with the key-value UserData
storage, providing a typed interface for reading and writing entity identifiers. The service interface is in the global
module, because it must be accessible to the client layer. The service implementation is located in the core module in the UserDataServiceBean
class. It delegates calls to the UserDataWorker
, in which the code that does the useful work is concentrated. This is done because this functionality is also required in the OperationEntityListener
, that is, “from the inside” of the middle layer. The service forms the “middleware border” and is intended only for calling from client blocks. It should not be called from the inside of the middle layer components, since this leads to the repeated triggering of the interceptor, which checks the authentication and processes the exception in a special way. And just for the sake of restoring order, it is worth separating the services called outside by middleware from the rest of the bins called from within. If only because when you call outside the transaction is always absent, and when you call from the middleware code, the transaction can already be opened.BalanceService
. It allows you to get the value of the account balance for an arbitrary date. Since this functionality is required both for clients in the UI and on the middle layer (test data generator), it is also moved to a separate bin BalanceWorker
.ReportService
. It retrieves the data for the report by category, and returns it as a list of instances of the CategoryAmount
non-persistent entity.SampleDataGenerator
bin is also implemented, which is intended for generating 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 as 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 with any external JMX tool. In our case, the bean has the SampleDataGeneratorMBean
interface, and it is registered in the core module spring.xml
.generateSampleData()
method of a bean is annotated as @Authenticated
. This means that when calling this method, a special system login will be executed and a user session will be present in the execution thread. It is required in this case because the method creates and modifies entities through EntityManager
, which, when saved, require the installation of their attributes createdBy
, updatedBy
- who updatedBy
given instances. On the other hand, the removeAllData()
method, also called via the JMX interface, does not require authentication because it removes data using SQL queries through the QueryRunner
and does not access the user session anywhere.DataWorker
, a bin to which the DataService
delegates the execution of CRUD operations with entities.FoldersPane
class is inherited from the platform FoldersPane
, the init()
and refreshFolders()
methods are overridden, in which the createBalancePanel()
method is createBalancePanel()
. It creates a new container, fills it with data from the BalanceService
, and fits it at the top of the parent container.LeftPanel
use LeftPanel
instead of the standard FoldersPane
, the AppWindow
class is inherited from the platform AkkAppWindow
and the createFoldersPane()
method is createFoldersPane()
.AkkAppWindow
used instead of the standard AppWindow
, the createAppWindow()
method of the App
class is overridden. In addition, the method of access to the new panel getLeftPanel()
is defined here - it is called from the screens to update the balance after a commit or delete operations.operation-browse.xml
is located in the operation-browse.xml
. Everything is standard here, except for the use of formatter classes to represent dates and amounts in the table of operations.DateFormatter
is used, which is sent to the format by a key from a package of localized messages. Thus, the format string can be different for different languages ​​- for Russian, the date is divided by dots, and for English - with characters /.DecimalFormatter
class was created in the project — it is used in the sums columns.Operation
entity — consumption with acc1
and amount1
, income with acc2
and amount2
. This variability could be implemented completely in the controller code, but I decided to make it more declarative - by separating the different parts of the screen into separate frames.iframe
component in the XML screen descriptor. It does not suit us, because we need to select the desired frame depending on the type of operation. Therefore, in the XML descriptor of the operation-edit.xml
, only the frame container is defined — the groupBox
component with the frameContainer
identifier, and the frame itself is created and inserted into the screen in the OperationEdit
controller: @Inject private GroupBoxLayout frameContainer; private OperationFrame operationFrame; @Override public void init(Map<String, Object> params) { ... String frameId = operation.getOpType().name().toLowerCase() + "-frame"; operationFrame = openFrame(frameContainer, frameId, params);
OperationFrame
— , . — .init()
OperationEdit
— , : @Override public void init(Map<String, Object> params) { ... getDsContext().addListener(new DsContext.CommitListenerAdapter() { @Override public void afterCommit(CommitContext context, Set<Entity> result) { LeftPanel leftPanel = App.getLeftPanel(); if (leftPanel != null) leftPanel.refreshBalance(); } }); }
expense-frame.xml
. textField
amountField
. ExpenseFrame
AmountCalculator
, : @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); … }
initAmount()
, BigDecimal
. datatype = decimal
, , . calculateAmount()
regexp, Groovy Scripting
. , .categories-report.xml
. , CategoryAmountDatasource
. datasourceClass
collectionDatasource
. JPQL-, , , . CategoryAmountDatasource
loadData()
DataService
JPQL-, ReportService
, : public class CategoryAmountDatasource extends CollectionDatasourceImpl<CategoryAmount, UUID> { private ReportService service = AppBeans.get(ReportService.NAME); @Override protected void loadData(Map<String, Object> params) { ... Date fromDate = (Date) params.get("from"); Date toDate = (Date) params.get("to"); ... List<CategoryAmount> list = service.getTurnoverByCategories(fromDate, toDate, categoryType, currency.getCode(), ids); for (CategoryAmount categoryAmount : list) { data.put(categoryAmount.getId(), categoryAmount); } ... }
refresh()
— . refreshDs1()
, refreshDs2()
CategoriesReport
. CategoryAmount
, data. , , CategoryAmount
, .categories-report.xml
— . excludeCategory
. XML- . How does this work? , init()
: . initExcludedCategories()
. “” , UserDataService
.ExcludeCategoryAction
excludeCategory()
, ComponentsFactory
-, , excludedBox
. , , , . , , .Source: https://habr.com/ru/post/235617/
All Articles