




gradlew setupTomcat deploygradlew startDbgradlew createDbgradlew startstartup.* 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